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
+31
-7
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Annotated
|
||||
|
||||
import httpx, vobject
|
||||
import time
|
||||
from pydantic import Field
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from starlette.applications import Starlette
|
||||
@@ -58,9 +59,15 @@ def _report_tasks(href, auth, inc=False):
|
||||
body = f'<?xml version="1.0"?><c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"><d:prop><d:getetag/><c:calendar-data/></d:prop><c:filter><c:comp-filter name="VCALENDAR">{filt}</c:comp-filter></c:filter></c:calendar-query>'
|
||||
return httpx.request("REPORT", RADICALE + href, content=body, auth=auth, headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=30).text
|
||||
|
||||
def _parse(xml, comp="VEVENT"):
|
||||
PARSE_DEADLINE_S = 12 # Wall-Clock-Limit fuer client-seitiges vobject-Parsen
|
||||
MAX_RESULTS = 300 # Output-Cap (sonst Token-Explosion bei tausenden Items)
|
||||
|
||||
|
||||
def _parse(xml, comp="VEVENT", t0=None, cap=None):
|
||||
objs = []
|
||||
for m in re.finditer(r'<(?:c|C):calendar-data[^>]*>(.*?)</(?:c|C):calendar-data>', xml, re.DOTALL):
|
||||
if cap is not None and len(objs) >= cap: break
|
||||
if t0 is not None and (time.monotonic() - t0) > PARSE_DEADLINE_S: break
|
||||
raw = m.group(1).replace("<","<").replace(">",">").replace("&","&")
|
||||
try:
|
||||
for c in vobject.readOne(raw).components():
|
||||
@@ -133,11 +140,16 @@ def get_events(
|
||||
if not cals: return f"Kalender '{calendar}' nicht gefunden. Nutze list_calendars."
|
||||
s = datetime.strptime(date_from, "%Y-%m-%d").strftime("%Y%m%dT000000Z")
|
||||
e = datetime.strptime(date_to, "%Y-%m-%d").strftime("%Y%m%dT235959Z")
|
||||
t0 = time.monotonic()
|
||||
results = []
|
||||
for cal in cals:
|
||||
for ev in _parse(_report(cal["href"], _auth(user), s, e)):
|
||||
for ev in _parse(_report(cal["href"], _auth(user), s, e), "VEVENT", t0=t0, cap=MAX_RESULTS - len(results)):
|
||||
results.append(_fmt_ev(ev, cal["name"]))
|
||||
return "\n\n".join(results) if results else "Keine Termine in diesem Zeitraum"
|
||||
if len(results) >= MAX_RESULTS or (time.monotonic() - t0) > PARSE_DEADLINE_S: break
|
||||
out = "\n\n".join(results) if results else "Keine Termine in diesem Zeitraum"
|
||||
if len(results) >= MAX_RESULTS:
|
||||
out += f"\n\n[Nur {MAX_RESULTS} Termine gezeigt — Zeitraum eingrenzen]"
|
||||
return out
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -154,13 +166,20 @@ def search_events(
|
||||
s = datetime.strptime(date_from, "%Y-%m-%d").strftime("%Y%m%dT000000Z")
|
||||
e = datetime.strptime(date_to, "%Y-%m-%d").strftime("%Y%m%dT235959Z")
|
||||
q = query.lower()
|
||||
t0 = time.monotonic()
|
||||
results = []
|
||||
truncated = False
|
||||
for cal in _discover(user, "VEVENT"):
|
||||
for ev in _parse(_report(cal["href"], _auth(user), s, e)):
|
||||
for ev in _parse(_report(cal["href"], _auth(user), s, e), "VEVENT", t0=t0):
|
||||
txt = f"{ev.summary.value if hasattr(ev,'summary') else ''} {ev.description.value if hasattr(ev,'description') else ''} {ev.location.value if hasattr(ev,'location') else ''}".lower()
|
||||
if q in txt:
|
||||
results.append(_fmt_ev(ev, cal["name"]))
|
||||
return "\n\n".join(results[:30]) if results else "Keine Treffer"
|
||||
if (time.monotonic() - t0) > PARSE_DEADLINE_S:
|
||||
truncated = True; break
|
||||
out = "\n\n".join(results[:30]) if results else "Keine Treffer"
|
||||
if truncated or len(results) > 30:
|
||||
out += "\n\n[Begrenzt (Top 30 / Zeit-Deadline) — Zeitraum oder Begriff praezisieren]"
|
||||
return out
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -182,11 +201,16 @@ def get_tasks(
|
||||
if not user: return "Error: not authenticated"
|
||||
lists = _discover(user, "VTODO")
|
||||
if task_list: lists = [l for l in lists if task_list.lower() in l["name"].lower()]
|
||||
t0 = time.monotonic()
|
||||
results = []
|
||||
for lst in lists:
|
||||
for t in _parse(_report_tasks(lst["href"], _auth(user), include_completed), "VTODO"):
|
||||
for t in _parse(_report_tasks(lst["href"], _auth(user), include_completed), "VTODO", t0=t0, cap=MAX_RESULTS - len(results)):
|
||||
results.append(_fmt_task(t, lst["name"]))
|
||||
return "\n\n".join(results) if results else "Keine Aufgaben"
|
||||
if len(results) >= MAX_RESULTS or (time.monotonic() - t0) > PARSE_DEADLINE_S: break
|
||||
out = "\n\n".join(results) if results else "Keine Aufgaben"
|
||||
if len(results) >= MAX_RESULTS:
|
||||
out += f"\n\n[Nur {MAX_RESULTS} Aufgaben gezeigt]"
|
||||
return out
|
||||
|
||||
|
||||
def _geocode(address):
|
||||
|
||||
Reference in New Issue
Block a user