From e40a6ff096da16cd79d99a655a30a82e736ec144 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Fri, 25 Sep 2026 08:43:42 +0200 Subject: [PATCH] Agent: ganzen Warteschlangen-Batch eines Typs in einem Zug drucken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Statt Job für Job (senden, auf Idle warten, nächster) holt der Agent nun ALLE eingereihten Jobs des eingelegten Formats und sendet alle mit gleichem Typ + Platzierung als EINE zusammenhängende write_svg-Sendung (clear beim ersten, start beim letzten Dokument). Der Plotter druckt so ohne Zwischenstopps durch. Reihenfolge bleibt exakt erhalten: stabile Sortierung -priority,date_created,id (= Erstell-/Auftragsnummern-Reihenfolge), die Batch-Bildung und Datei-Verkettung ändern die relative Reihenfolge nicht; seiten_reihenfolge wirkt nur je Brief. So passen Schriftstücke, Kuverts und Sammel-PDF weiterhin zueinander. Abschluss markiert alle Batch-Jobs. Co-Authored-By: Claude Opus 4.8 --- agent.py | 213 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 118 insertions(+), 95 deletions(-) diff --git a/agent.py b/agent.py index af22ef6..b969e2e 100644 --- a/agent.py +++ b/agent.py @@ -284,9 +284,9 @@ def main(): current_format = (maschine.get("current_format") or "").strip() # Format-Warteschlange: nur passendes Format, kein Auto-Anschluss. if current_format: - job = _naechster_job(dx, machine_id, current_format) - if job: - laufend = _sende(dx, plotter, job, KIND) + jobs = _naechste_jobs(dx, machine_id, current_format) + if jobs: + laufend = _sende_batch(dx, plotter, jobs, KIND) time.sleep(poll) @@ -371,127 +371,150 @@ def _heartbeat(dx, machine_id, status): pass -def _naechster_job(dx, machine_id, current_format): - jobs = dx.get( +def _naechste_jobs(dx, machine_id, current_format): + """ALLE eingereihten Jobs des aktuell eingelegten Formats – in stabiler + Warteschlangen-Reihenfolge (Priorität, dann Erstellzeit, dann ID als + eindeutiger Tiebreak). Diese Reihenfolge = Erstell-/Auftragsnummern-Reihenfolge + (enqueue-bulk legt die Jobs nach Auftragsnummer an) und MUSS exakt erhalten + bleiben, damit Schriftstücke, Kuverts und die Sammel-PDF zueinander passen.""" + return dx.get( "/items/jobs", **{ "filter[machine][_eq]": machine_id, "filter[status][_eq]": "queued", - # Nur Jobs des aktuell eingelegten Formats. "filter[format][_eq]": current_format, - "sort": "-priority,date_created", - "limit": 1, + "sort": "-priority,date_created,id", + "limit": -1, "fields": "id,type,output_ref,params,numbers,spaltenweise," "order.id,order.order_number,order.needs_envelope," "template.width,template.height," "template.xpos,template.ypos,template.scale,template.rotation", }, - ) - return jobs[0] if jobs else None + ) or [] -def _sende(dx, plotter, job, KIND): - """Sendet einen Job an die Maschine und markiert ihn als „printing". - - Gibt einen Kontext zum späteren Abschluss zurück (oder None bei Fehler). - Es wird NICHT auf das Druckende gewartet – das übernimmt die Hauptschleife - (erst wenn die Maschine wieder „Idle" ist, wird der nächste Job gesendet). - """ - job_id = job["id"] - order = job.get("order") or {} - order_id = order.get("id") - # Ordner: entweder Auftragsnummer oder ein output_ref (z. B. Testdruck). - order_nr = job.get("output_ref") or order.get("order_number") +def _eff_tpl(job): + """Effektives Maschinen-Template: Template-Werte + Pro-Druck-Übersteuerung.""" tpl = dict(job.get("template") or {}) - # Pro-Druck-Übersteuerung der Maße (aus dem Druck-Popup) hat Vorrang. params = job.get("params") if isinstance(params, dict): for k in ("width", "height", "xpos", "ypos", "scale", "rotation"): if params.get(k) is not None: tpl[k] = params[k] - nummern = parse_nummern(job.get("numbers")) + return tpl + + +def _batch_key(job): + """Jobs mit gleichem Typ UND gleicher Platzierung dürfen zusammen in EINEN + write_svg-Batch (die Maschine kennt nur eine Platzierung je Sendung).""" + return (job.get("type") or "brief", + json.dumps(_eff_tpl(job), sort_keys=True, default=str)) + + +def _job_dateien(dx, job, KIND): + """Lädt die SVG-Dateien EINES Jobs (Typ-passend, Nummern-Filter, Seiten- + Reihenfolge, ggf. spaltenweise). Gibt [(name, bytes)] zurück; wirft bei Fehler.""" + order = job.get("order") or {} + order_nr = job.get("output_ref") or order.get("order_number") typ = job.get("type") or "brief" - print(f"[Agent] Job {job_id}: {typ} für {order_nr} → an Maschine senden") + if not order_nr: + raise RuntimeError("Auftragsnummer fehlt am Job.") + liste = dx.get_raw(f"/skrift-orders/files/{order_nr}") or {} + gewuenscht = KIND.get(typ, "schriftstueck") + passende = sorted( + [f for f in (liste.get("files") or []) if f.get("kind") == gewuenscht], + key=lambda f: f.get("name", ""), + ) + # Nur bestimmte Briefnummern (1-basiert, positionsbasiert), falls angegeben. + nummern = parse_nummern(job.get("numbers")) + if nummern is not None: + passende = [f for i, f in enumerate(passende, start=1) if i in nummern] + if not passende: + raise RuntimeError(f"Keine Dateien vom Typ '{gewuenscht}' für {order_nr} gefunden.") + # Mehrseitige Briefe: Seiten je Brief absteigend (Stapel-Reihenfolge). + passende = seiten_reihenfolge(passende) + # Rollenschonende Zeichenreihenfolge (spaltenweise) nur fürs Schriftstück. + reorder = bool(job.get("spaltenweise")) and gewuenscht == "schriftstueck" + dateien = [] + for f in passende: + p = {"rel": f["rel"]} + if reorder: + p["reorder"] = "column" + dateien.append((f["name"], dx.download(f"/skrift-orders/files/{order_nr}/download", p))) + return dateien - dx.patch(f"/items/jobs/{job_id}", {"status": "printing", "error": None}) - # Produktionsstatus fürs Kanban: sobald gedruckt wird → „im_druck". - if order_id: - try: dx.patch(f"/items/orders/{order_id}", {"production_status": "im_druck"}) - except Exception: pass + +def _sende_batch(dx, plotter, jobs, KIND): + """Sendet ALLE Jobs mit gleichem Typ+Platzierung wie der erste als EINEN + zusammenhängenden Druck an die Maschine (clear beim ersten, start beim letzten + Dokument). Markiert alle als „printing". Gibt einen Sammel-Kontext zum späteren + Abschluss zurück (oder None, wenn nichts gesendet wurde).""" + ref = _batch_key(jobs[0]) + batch = [j for j in jobs if _batch_key(j) == ref] + tpl = _eff_tpl(batch[0]) + dateien = [] + infos = [] + for job in batch: + jid = job["id"] + try: + teil = _job_dateien(dx, job, KIND) + except Exception as e: + dx.patch(f"/items/jobs/{jid}", {"status": "failed", "error": str(e)[:1000]}) + print(f"[Agent] Job {jid} übersprungen: {e}") + continue + order = job.get("order") or {} + oid = order.get("id") + dx.patch(f"/items/jobs/{jid}", {"status": "printing", "error": None}) + if oid: + try: dx.patch(f"/items/orders/{oid}", {"production_status": "im_druck"}) + except Exception: pass + dateien.extend(teil) + infos.append({"job_id": jid, "order_id": oid, + "order_nr": job.get("output_ref") or order.get("order_number"), + "typ": job.get("type") or "brief"}) + if not dateien: + return None try: - if not order_nr: - raise RuntimeError("Auftragsnummer fehlt am Job.") - liste = dx.get_raw(f"/skrift-orders/files/{order_nr}") or {} - gewuenscht = KIND.get(typ, "schriftstueck") - passende = sorted( - [f for f in (liste.get("files") or []) if f.get("kind") == gewuenscht], - key=lambda f: f.get("name", ""), - ) - # Nur bestimmte Briefnummern drucken, falls im Job angegeben. - # Positionsbasiert (1 = erstes Dokument), NICHT über die Zahl im - # Dateinamen: Alt-Aufträge sind 0-basiert (letter_000.svg), neue - # 1-basiert (letter_001.svg). Die Position stimmt in beiden Fällen. - if nummern is not None: - passende = [f for i, f in enumerate(passende, start=1) if i in nummern] - if not passende: - raise RuntimeError(f"Keine Dateien vom Typ '{gewuenscht}' für {order_nr} gefunden.") - - # Mehrseitige Briefe: Seiten je Brief absteigend an den Plotter (Stapel- - # Reihenfolge, damit Seite 1 am Ende oben liegt). Einseitige unverändert. - passende = seiten_reihenfolge(passende) - - # Rollenschonende Zeichenreihenfolge (spaltenweise) nur fürs Schriftstück - # (A6 quer). Das Backend liefert das SVG dann umsortiert; Kuverts bleiben - # unverändert. - reorder = bool(job.get("spaltenweise")) and gewuenscht == "schriftstueck" - dateien = [] - for f in passende: - params = {"rel": f["rel"]} - if reorder: - params["reorder"] = "column" - inhalt = dx.download(f"/skrift-orders/files/{order_nr}/download", params) - dateien.append((f["name"], inhalt)) - plotter.login() plotter.write(dateien, tpl) - print(f"[Agent] Job {job_id} an Maschine übergeben ({len(dateien)} Dateien) – warte auf Fertigstellung.") - return {"job_id": job_id, "order_id": order_id, "order_nr": order_nr, - "typ": typ, "gestartet": False, "ts": time.monotonic()} except Exception as e: - dx.patch(f"/items/jobs/{job_id}", {"status": "failed", "error": str(e)[:1000]}) - print(f"[Agent] Job {job_id} fehlgeschlagen: {e}") + for info in infos: + dx.patch(f"/items/jobs/{info['job_id']}", {"status": "failed", "error": str(e)[:1000]}) + print(f"[Agent] Batch fehlgeschlagen: {e}") return None + print(f"[Agent] Batch: {len(infos)} Job(s) / {len(dateien)} Dateien in einem Zug an die Maschine – warte auf Fertigstellung.") + return {"jobs": infos, "gestartet": False, "ts": time.monotonic()} def _abschluss(dx, ctx): - """Schließt einen fertig gedruckten Job ab: Status, Indikatoren, Kuvertierung. - - Wird erst aufgerufen, wenn die Maschine nach dem Druck wieder „Idle" meldet. - """ - job_id = ctx["job_id"] - order_id = ctx.get("order_id") - order_nr = ctx.get("order_nr") - typ = ctx.get("typ") - try: - dx.patch(f"/items/jobs/{job_id}", {"status": "printed"}) - except Exception as e: - print(f"[Agent] Konnte Job {job_id} nicht auf 'printed' setzen: {e}") - # Indikator am Auftrag setzen (Schriftstück bzw. Umschlag gedruckt) und - # bei Vollständigkeit automatisch in die Kuvertierung schieben. - if order_id: - feld = "kuvert_gedruckt" if typ == "umschlag" else "brief_gedruckt" + """Schließt ALLE Jobs eines fertig gedruckten Batches ab: Status, Indikatoren, + Kuvertierung. Wird erst aufgerufen, wenn die Maschine nach dem Druck wieder + „Idle" meldet (der ganze Batch war EINE Sendung).""" + for info in ctx.get("jobs", []): + job_id = info["job_id"] + order_id = info.get("order_id") + order_nr = info.get("order_nr") + typ = info.get("typ") try: - dx.patch(f"/items/orders/{order_id}", {feld: True}) - stand = dx.get(f"/items/orders/{order_id}", - fields="brief_gedruckt,kuvert_gedruckt,needs_envelope,production_status") or {} - braucht_kuvert = bool(stand.get("needs_envelope")) - fertig = stand.get("brief_gedruckt") and (stand.get("kuvert_gedruckt") or not braucht_kuvert) - if fertig and stand.get("production_status") == "im_druck": - dx.patch(f"/items/orders/{order_id}", {"production_status": "kuvertieren"}) - print(f"[Agent] Auftrag {order_nr} vollständig gedruckt → Kuvertierung.") - except Exception: - pass - print(f"[Agent] Job {job_id} fertig gedruckt.") + dx.patch(f"/items/jobs/{job_id}", {"status": "printed"}) + except Exception as e: + print(f"[Agent] Konnte Job {job_id} nicht auf 'printed' setzen: {e}") + # Indikator am Auftrag setzen (Schriftstück bzw. Umschlag gedruckt) und + # bei Vollständigkeit automatisch in die Kuvertierung schieben. + if order_id: + feld = "kuvert_gedruckt" if typ == "umschlag" else "brief_gedruckt" + try: + dx.patch(f"/items/orders/{order_id}", {feld: True}) + stand = dx.get(f"/items/orders/{order_id}", + fields="brief_gedruckt,kuvert_gedruckt,needs_envelope,production_status") or {} + braucht_kuvert = bool(stand.get("needs_envelope")) + fertig = stand.get("brief_gedruckt") and (stand.get("kuvert_gedruckt") or not braucht_kuvert) + if fertig and stand.get("production_status") == "im_druck": + dx.patch(f"/items/orders/{order_id}", {"production_status": "kuvertieren"}) + print(f"[Agent] Auftrag {order_nr} vollständig gedruckt → Kuvertierung.") + except Exception: + pass + print(f"[Agent] Job {job_id} fertig gedruckt.") if __name__ == "__main__":