fix(files): Bilder korrekt zuordnen bei Queue (clientMsgId-Korrelation)
Die App schickt Datei (fire-and-forget) und Text (ACK-getrackt, bei Queue verzoegert) als getrennte RVS-Nachrichten; die Datei trug keine clientMsgId. Die Bridge mergte gepufferte Files rein per Timing an den naechsten Text → bei mehreren schnellen Nachrichten landeten Bilder beim falschen Text. App: file-Send traegt jetzt dieselbe clientMsgId wie der Text. Bridge: puffert Files mit cmid und merged beim Text-Flush NUR die Files mit passender cmid; Rest bleibt fuer seine eigene Nachricht gepuffert. Fallback (Legacy/kein Treffer) = altes Verhalten, damit nie ein Bild verloren geht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2371,6 +2371,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
size: file.size,
|
size: file.size,
|
||||||
base64,
|
base64,
|
||||||
projectId: activePid,
|
projectId: activePid,
|
||||||
|
// Korrelation: dieselbe clientMsgId wie der Text, damit die Bridge
|
||||||
|
// die Datei genau DIESER Nachricht zuordnet — auch wenn Files (fire-
|
||||||
|
// and-forget) und Text (ACK-getrackt, bei Queue verzoegert) desyncen.
|
||||||
|
...(cmid && { clientMsgId: cmid }),
|
||||||
...(isPhoto && file.width && { width: file.width, height: file.height }),
|
...(isPhoto && file.width && { width: file.width, height: file.height }),
|
||||||
...(location && { location }),
|
...(location && { location }),
|
||||||
});
|
});
|
||||||
|
|||||||
+48
-17
@@ -790,7 +790,8 @@ class ARIABridge:
|
|||||||
# Anfrage an aria-core. Sonst antwortet ARIA zweimal (einmal "warte auf
|
# Anfrage an aria-core. Sonst antwortet ARIA zweimal (einmal "warte auf
|
||||||
# Anweisung" beim file, einmal auf den Chat-Text).
|
# Anweisung" beim file, einmal auf den Chat-Text).
|
||||||
# Liste von Tuples: (file_path, name, file_type, size_kb, width, height)
|
# Liste von Tuples: (file_path, name, file_type, size_kb, width, height)
|
||||||
self._pending_files: list[tuple[str, str, str, int, int, int]] = []
|
# (file_path, name, type, kb, width, height, clientMsgId)
|
||||||
|
self._pending_files: list[tuple[str, str, str, int, int, int, str]] = []
|
||||||
self._pending_files_flush_task: Optional[asyncio.Task] = None
|
self._pending_files_flush_task: Optional[asyncio.Task] = None
|
||||||
# Projekt-Kontext der gerade gepufferten Anhaenge (aus dem file-Upload).
|
# Projekt-Kontext der gerade gepufferten Anhaenge (aus dem file-Upload).
|
||||||
# Wird beim Flush an send_to_core gegeben, damit Anhaenge im richtigen
|
# Wird beim Flush an send_to_core gegeben, damit Anhaenge im richtigen
|
||||||
@@ -1839,16 +1840,16 @@ class ARIABridge:
|
|||||||
return " ".join(parts) + " " + text
|
return " ".join(parts) + " " + text
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def _build_pending_files_message(self, user_text: str) -> str:
|
def _build_pending_files_message(self, user_text: str, files: list) -> str:
|
||||||
"""Baut eine Anweisung an aria-core aus den gepufferten Files + optionalem
|
"""Baut eine Anweisung an aria-core aus den uebergebenen Files + optionalem
|
||||||
User-Text. user_text leer → 'warte auf Anweisung'-Variante."""
|
User-Text. user_text leer → 'warte auf Anweisung'-Variante."""
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
for fp, name, ftype, kb, w, h in self._pending_files:
|
for fp, name, ftype, kb, w, h, _cmid in files:
|
||||||
dim = f" {w}x{h}px" if (w and h) else ""
|
dim = f" {w}x{h}px" if (w and h) else ""
|
||||||
kind = "Bild" if ftype.startswith("image/") else "Datei"
|
kind = "Bild" if ftype.startswith("image/") else "Datei"
|
||||||
parts.append(f"- {kind}: {name}{dim} ({ftype}, {kb}KB) liegt unter {fp}")
|
parts.append(f"- {kind}: {name}{dim} ({ftype}, {kb}KB) liegt unter {fp}")
|
||||||
files_summary = "\n".join(parts)
|
files_summary = "\n".join(parts)
|
||||||
n = len(self._pending_files)
|
n = len(files)
|
||||||
anhang = "Anhang" if n == 1 else "Anhaenge"
|
anhang = "Anhang" if n == 1 else "Anhaenge"
|
||||||
if user_text:
|
if user_text:
|
||||||
return (f"Stefan hat dir {n} {anhang} geschickt:\n{files_summary}\n\n"
|
return (f"Stefan hat dir {n} {anhang} geschickt:\n{files_summary}\n\n"
|
||||||
@@ -1857,15 +1858,16 @@ class ARIABridge:
|
|||||||
f"Warte auf seine Anweisung was du damit tun sollst.")
|
f"Warte auf seine Anweisung was du damit tun sollst.")
|
||||||
|
|
||||||
async def _flush_pending_files_after(self, delay: float) -> None:
|
async def _flush_pending_files_after(self, delay: float) -> None:
|
||||||
"""Wenn nach `delay`s kein chat-Text gekommen ist: Files alleine an
|
"""Wenn nach `delay`s kein chat-Text gekommen ist: alle noch gepufferten
|
||||||
aria-core senden ('warte auf Anweisung'-Variante)."""
|
Files alleine an aria-core senden ('warte auf Anweisung'-Variante)."""
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
return
|
return
|
||||||
if not self._pending_files:
|
if not self._pending_files:
|
||||||
return
|
return
|
||||||
text = self._build_pending_files_message("")
|
files = self._pending_files
|
||||||
|
text = self._build_pending_files_message("", files)
|
||||||
self._pending_files = []
|
self._pending_files = []
|
||||||
self._pending_files_flush_task = None
|
self._pending_files_flush_task = None
|
||||||
pid = self._pending_files_project_id
|
pid = self._pending_files_project_id
|
||||||
@@ -1873,22 +1875,47 @@ class ARIABridge:
|
|||||||
await self.send_to_core(text, source="app-file", project_id=pid)
|
await self.send_to_core(text, source="app-file", project_id=pid)
|
||||||
|
|
||||||
async def _flush_pending_files_with_text(self, user_text: str,
|
async def _flush_pending_files_with_text(self, user_text: str,
|
||||||
project_id: str = "") -> bool:
|
project_id: str = "",
|
||||||
|
client_msg_id: str = "") -> bool:
|
||||||
"""Wenn ein chat-Text reinkommt waehrend Files gepuffert sind:
|
"""Wenn ein chat-Text reinkommt waehrend Files gepuffert sind:
|
||||||
Files + Text zu einer einzigen aria-core-Nachricht mergen.
|
Files + Text zu einer einzigen aria-core-Nachricht mergen.
|
||||||
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden).
|
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden).
|
||||||
|
|
||||||
|
KORRELATION (Fix Queue-Bug): Files tragen dieselbe clientMsgId wie ihr
|
||||||
|
Text. Bei einer Queue gehen Files (fire-and-forget) und Text (ACK-
|
||||||
|
getrackt, ggf. verzoegert) auseinander — ohne Korrelation landeten die
|
||||||
|
Bilder beim falschen Text. Wir mergen darum NUR die Files mit passender
|
||||||
|
cmid; der Rest bleibt gepuffert fuer seine eigene Nachricht. Fallback
|
||||||
|
(Legacy-App ohne cmid an Files, oder cmid ohne Treffer): altes Verhalten
|
||||||
|
(alle Files mit diesem Text), damit nie ein Bild verloren geht.
|
||||||
|
|
||||||
project_id: Projekt-Kontext aus dem chat-Payload (der sichtbare Focus
|
project_id: Projekt-Kontext aus dem chat-Payload (der sichtbare Focus
|
||||||
beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext
|
beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext zurueck.
|
||||||
zurueck, damit Anhaenge im richtigen Projekt landen statt im Hauptchat."""
|
"""
|
||||||
if not self._pending_files:
|
if not self._pending_files:
|
||||||
return False
|
return False
|
||||||
|
cmid = (client_msg_id or "").strip()
|
||||||
|
matching = [f for f in self._pending_files if cmid and f[6] == cmid]
|
||||||
|
if not matching:
|
||||||
|
# Kein cmid-Treffer → altes Verhalten: alle gepufferten Files mergen.
|
||||||
|
matching = list(self._pending_files)
|
||||||
|
remaining: list = []
|
||||||
|
else:
|
||||||
|
remaining = [f for f in self._pending_files if f not in matching]
|
||||||
|
|
||||||
|
text = self._build_pending_files_message(user_text, matching)
|
||||||
|
self._pending_files = remaining
|
||||||
|
pid = (project_id or "").strip() or self._pending_files_project_id
|
||||||
|
# Flush-Timer neu setzen wenn noch Files anderer Nachrichten warten,
|
||||||
|
# sonst zuruecksetzen.
|
||||||
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
||||||
self._pending_files_flush_task.cancel()
|
self._pending_files_flush_task.cancel()
|
||||||
self._pending_files_flush_task = None
|
self._pending_files_flush_task = None
|
||||||
text = self._build_pending_files_message(user_text)
|
if remaining:
|
||||||
self._pending_files = []
|
self._pending_files_flush_task = asyncio.create_task(
|
||||||
pid = (project_id or "").strip() or self._pending_files_project_id
|
self._flush_pending_files_after(self._PENDING_FILES_WINDOW_SEC)
|
||||||
|
)
|
||||||
|
else:
|
||||||
self._pending_files_project_id = ""
|
self._pending_files_project_id = ""
|
||||||
# create_task statt await — sonst blockt der RVS-recv-Loop bis Brain
|
# create_task statt await — sonst blockt der RVS-recv-Loop bis Brain
|
||||||
# fertig ist (siehe chat-handler oben).
|
# fertig ist (siehe chat-handler oben).
|
||||||
@@ -2364,7 +2391,8 @@ class ARIABridge:
|
|||||||
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
|
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
|
||||||
# zwei separater send_to_core-Calls.
|
# zwei separater send_to_core-Calls.
|
||||||
merged = await self._flush_pending_files_with_text(
|
merged = await self._flush_pending_files_with_text(
|
||||||
text, project_id=str(payload.get("projectId") or ""))
|
text, project_id=str(payload.get("projectId") or ""),
|
||||||
|
client_msg_id=client_msg_id or "")
|
||||||
if merged:
|
if merged:
|
||||||
logger.info("[rvs] App-Chat (mit Anhaengen) project=%s: '%s'",
|
logger.info("[rvs] App-Chat (mit Anhaengen) project=%s: '%s'",
|
||||||
str(payload.get("projectId") or "") or "(main)", text[:80])
|
str(payload.get("projectId") or "") or "(main)", text[:80])
|
||||||
@@ -2658,8 +2686,11 @@ class ARIABridge:
|
|||||||
logger.warning("[rvs] Bild-Resize fehlgeschlagen (%s) — Original wird genutzt: %s",
|
logger.warning("[rvs] Bild-Resize fehlgeschlagen (%s) — Original wird genutzt: %s",
|
||||||
file_name, e)
|
file_name, e)
|
||||||
|
|
||||||
# In Pending-Queue + Flush-Timer (anti-spam Buffering)
|
# In Pending-Queue + Flush-Timer (anti-spam Buffering).
|
||||||
self._pending_files.append((file_path, file_name, file_type, size_kb, int(width or 0), int(height or 0)))
|
# clientMsgId mitpuffern → spaeterer Text-Flush ordnet die Datei
|
||||||
|
# genau SEINER Nachricht zu (Queue-Korrelation, s. _flush_*).
|
||||||
|
file_cmid = str(payload.get("clientMsgId") or "")
|
||||||
|
self._pending_files.append((file_path, file_name, file_type, size_kb, int(width or 0), int(height or 0), file_cmid))
|
||||||
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
||||||
self._pending_files_flush_task.cancel()
|
self._pending_files_flush_task.cancel()
|
||||||
self._pending_files_flush_task = asyncio.create_task(
|
self._pending_files_flush_task = asyncio.create_task(
|
||||||
|
|||||||
Reference in New Issue
Block a user