Agent: ganzen Warteschlangen-Batch eines Typs in einem Zug drucken

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 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-25 08:43:42 +02:00
parent 6fe3e9a34a
commit e40a6ff096

213
agent.py
View File

@@ -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__":