From 2783e51f447d39b58bb79a9c6fb3119a31d24484 Mon Sep 17 00:00:00 2001 From: Floke Date: Fri, 6 Mar 2026 14:46:41 +0000 Subject: [PATCH] fix: [30388f42] Stoppe Webhook-Loop durch Idempotenz und Truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Führt clean_text_for_so Hilfsfunktion ein, die KI-Texte auf 200 Zeichen kürzt. - Vergleicht und sendet nur noch gekürzte Texte an SuperOffice, um Differenzen durch serverseitige Kürzung zu vermeiden. - Fügt detailliertes Logging für erkannte Änderungen hinzu. - Verhindert so die Endlosschleife bei Benutzern mit ID 528 (Bot-ID). --- connector-superoffice/worker.py | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/connector-superoffice/worker.py b/connector-superoffice/worker.py index 08ab8bcb..1fa3bab0 100644 --- a/connector-superoffice/worker.py +++ b/connector-superoffice/worker.py @@ -33,6 +33,12 @@ def safe_get_udfs(entity_data): logger.error(f"Error reading UDFs: {e}") return {} +def clean_text_for_so(text, limit=200): + """Clean and truncate text for SuperOffice UDF compatibility.""" + if not text or text == "null": return "" + # Strip whitespace and truncate to safe limit + return str(text).strip()[:limit] + def process_job(job, so_client: SuperOfficeClient, queue: JobQueue): """ Core logic for processing a single job. @@ -46,9 +52,10 @@ def process_job(job, so_client: SuperOfficeClient, queue: JobQueue): # --- CIRCUIT BREAKER: DETECT ECHOES --- # We log if the event was triggered by our own API user (Associate 528) # but we NO LONGER SKIP IT, to allow manual changes by the user who shares the same ID. + # The idempotency logic below will prevent infinite loops. changed_by = payload.get("ChangedByAssociateId") if changed_by == 528: - logger.info(f"ℹ️ Potential Echo: Event triggered by Associate 528. Proceeding to allow manual user updates.") + logger.info(f"ℹ️ Potential Echo: Event triggered by Associate 528. Proceeding to check for meaningful changes.") # -------------------------------------------- # 0. ID Extraction & Early Exit for irrelevant jobs @@ -287,23 +294,25 @@ def process_job(job, so_client: SuperOfficeClient, queue: JobQueue): contact_patch["OrgNr"] = ce_vat # --- C. AI Openers & Summary Sync --- - ce_opener = provisioning_data.get("opener") - ce_opener_secondary = provisioning_data.get("opener_secondary") - ce_summary = provisioning_data.get("summary") + # TRUNCATION TO 200 CHARS TO PREVENT INFINITE LOOPS (SO UDF LIMITS) + ce_opener = clean_text_for_so(provisioning_data.get("opener"), limit=200) + ce_opener_secondary = clean_text_for_so(provisioning_data.get("opener_secondary"), limit=200) + ce_summary = clean_text_for_so(provisioning_data.get("summary"), limit=132) # Summary is very short in SO - if ce_opener and ce_opener != "null" and current_udfs.get(settings.UDF_OPENER) != ce_opener: + if ce_opener and current_udfs.get(settings.UDF_OPENER) != ce_opener: + logger.info(f"Change detected: Opener Primary (Length: {len(ce_opener)})") if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {} contact_patch["UserDefinedFields"][settings.UDF_OPENER] = ce_opener - if ce_opener_secondary and ce_opener_secondary != "null" and current_udfs.get(settings.UDF_OPENER_SECONDARY) != ce_opener_secondary: + + if ce_opener_secondary and current_udfs.get(settings.UDF_OPENER_SECONDARY) != ce_opener_secondary: + logger.info(f"Change detected: Opener Secondary (Length: {len(ce_opener_secondary)})") if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {} contact_patch["UserDefinedFields"][settings.UDF_OPENER_SECONDARY] = ce_opener_secondary - if ce_summary and ce_summary != "null": - short_summary = (ce_summary[:132] + "...") if len(ce_summary) > 135 else ce_summary - if current_udfs.get(settings.UDF_SUMMARY) != short_summary: - logger.info("Change detected: AI Summary") - if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {} - contact_patch["UserDefinedFields"][settings.UDF_SUMMARY] = short_summary + if ce_summary and current_udfs.get(settings.UDF_SUMMARY) != ce_summary: + logger.info(f"Change detected: AI Summary (Length: {len(ce_summary)})") + if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {} + contact_patch["UserDefinedFields"][settings.UDF_SUMMARY] = ce_summary # --- D. Timestamps & Website Sync --- # CRITICAL: We only update the timestamp if we actually have OTHER changes to push. @@ -326,6 +335,8 @@ def process_job(job, so_client: SuperOfficeClient, queue: JobQueue): logger.info(f"Pushing combined PATCH for Contact {contact_id}: {list(contact_patch.keys())}") so_client.patch_contact(contact_id, contact_patch) logger.info("✅ Contact Update Successful.") + else: + logger.info(f"ℹ️ No changes detected for Contact {contact_id}. Skipping PATCH.") # 2d. Sync Person Position role_name = provisioning_data.get("role_name")