harden: per-item robustness + deadlines/caps across connectors; layered tests
Mail: _safe_decode + _iter_messages (per-mail try/except) -> eine kaputte Mail (charset x-unknown) bricht die Suche nicht mehr ab, findet so auch uralte Mails. Vollscan + Datum-Sort + MAX_SCAN/DEADLINE_S (Selbstheilung, kein 24h-Haenger mehr). Files: search_files mit SEARCH_DEADLINE_S + SEARCH_MAX_DIRS begrenzt (war unbegrenzte rekursive PROPFIND -> real 35s/449 Calls). Calendar/Contacts: PARSE_DEADLINE_S fuer client-seitiges vobject-Parsen; MAX_RESULTS-Output-Cap in get_events/get_tasks (war 10939 Zeilen bei 6000 Events); search Top-N + Hinweise. Tests: in Schichten getrennt -> Smoke (nightly), Edge + Stress (on-demand). Neu: test_smoke/test_edge/test_stress + conftest-Marker + run_full_tests.sh, nightly laeuft nur Smoke. Kontakt-Foto-Roundtrip-Test, Mail-Edge-Mails (x-unknown, kaputter Header, 2009er Alt-Mail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
936ebc2f56
commit
c9ccb4392b
+16
-3
@@ -4,6 +4,7 @@ import os, sys, re, contextlib, uuid, base64
|
||||
from typing import Annotated
|
||||
|
||||
import httpx, vobject
|
||||
import time
|
||||
from pydantic import Field
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.types import TextContent, ImageContent
|
||||
@@ -38,11 +39,15 @@ def _discover_ab(user):
|
||||
books.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
|
||||
return books
|
||||
|
||||
def _get_contacts(href, auth):
|
||||
PARSE_DEADLINE_S = 12 # Wall-Clock-Limit fuer client-seitiges vobject-Parsen
|
||||
|
||||
|
||||
def _get_contacts(href, auth, t0=None):
|
||||
body = '<?xml version="1.0"?><c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:getetag/><c:address-data/></d:prop></c:addressbook-query>'
|
||||
r = httpx.request("REPORT", RADICALE + href, content=body, auth=auth, headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=60)
|
||||
contacts = []
|
||||
for m in re.finditer(r'<(?:c|CR):address-data[^>]*>(.*?)</(?:c|CR):address-data>', r.text, re.DOTALL):
|
||||
if t0 is not None and (time.monotonic() - t0) > PARSE_DEADLINE_S: break
|
||||
raw = m.group(1).replace("<","<").replace(">",">").replace("&","&")
|
||||
try: contacts.append(vobject.readOne(raw))
|
||||
except: pass
|
||||
@@ -98,14 +103,22 @@ def search_contacts(
|
||||
user = get_current_user()
|
||||
if not user: return "Error: not authenticated"
|
||||
q = query.lower()
|
||||
t0 = time.monotonic()
|
||||
results = []
|
||||
truncated = False
|
||||
for book in _discover_ab(user):
|
||||
for c in _get_contacts(book["href"], _auth(user)):
|
||||
for c in _get_contacts(book["href"], _auth(user), t0=t0):
|
||||
txt = " ".join(str(getattr(c, a, None) or '') for a in ['fn','email','tel','org','note','nickname']).lower()
|
||||
if q in txt:
|
||||
results.append(_brief(c))
|
||||
if len(results) >= limit: break
|
||||
return "\n\n".join(results) if results else "Keine Kontakte gefunden"
|
||||
if len(results) >= limit: break
|
||||
if (time.monotonic() - t0) > PARSE_DEADLINE_S:
|
||||
truncated = True; break
|
||||
out = "\n\n".join(results) if results else "Keine Kontakte gefunden"
|
||||
if truncated:
|
||||
out += "\n\n[Suche zeitbegrenzt — Begriff praezisieren]"
|
||||
return out
|
||||
|
||||
|
||||
def _get_photo_data(card):
|
||||
|
||||
Reference in New Issue
Block a user