feat: App-Chat-Sync — verpasste Nachrichten + chat_cleared Live-Update
Zwei zusammenhaengende Bugs:
1. App aktualisierte nicht wenn die Diagnostic "Konversation komplett
zuruecksetzen" gedrueckt hat — die App hatte den lokalen Stand weiter
2. Nachrichten die kamen waehrend die App offline/geschlossen war,
wurden nicht nachgeladen
Loesung: chat_backup.jsonl wird wieder geschrieben (Bridge statt Diagnostic,
weil OpenClaw-Code-Pfad tot ist) und dient als Server-Truth fuer App+Diagnostic.
bridge/aria_bridge.py
_append_chat_backup() schreibt jeden Turn (User + ARIA) als JSONL-Zeile
in /shared/config/chat_backup.jsonl. Trigger: send_to_core (User) +
_process_core_response (Assistant, inkl. file-Attachments).
_read_chat_backup_since(since_ms, limit) liest die Datei, filtert auf
ts > since_ms, gibt max limit neueste zurueck. Honoriert file_deleted-Marker.
Neuer RVS-Handler chat_history_request {since, limit?} → antwortet mit
chat_history_response {messages: [...], since}.
diagnostic/server.js
/api/chat-history-clear broadcastet jetzt zusaetzlich chat_cleared via
RVS (sendToRVS_raw), damit App ihre lokale Liste auch leert. Vorher nur
Browser-Clients via broadcast() — App war aussen vor.
rvs/server.js
ALLOWED_TYPES um chat_history_request, chat_history_response, chat_cleared.
android/src/screens/ChatScreen.tsx
- Bei (re)connect: AsyncStorage 'aria_chat_last_sync' lesen → send
chat_history_request {since}
- Handler chat_history_response: incoming → ChatMessage[] mappen,
Attachments aus 'files'-Array rekonstruieren, mergen (Dedup via timestamp),
lastSync hochziehen
- Handler chat_cleared: setMessages([]) + AsyncStorage 'chat_messages' +
'last_sync' weg
- Bei jeder eingehenden chat-Message: 'aria_chat_last_sync' updaten damit
Reconnect nicht doppelt nachzieht
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -396,6 +396,52 @@ const ChatScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
// skill_created: ARIA hat einen neuen Skill angelegt → eigene Bubble
|
||||
// chat_cleared: Diagnostic hat die History komplett geleert
|
||||
// → lokal auch loeschen (visuell + Persistenz)
|
||||
if (message.type === 'chat_cleared') {
|
||||
console.log('[Chat] chat_cleared — leere lokale Anzeige + Storage');
|
||||
setMessages([]);
|
||||
AsyncStorage.removeItem(CHAT_STORAGE_KEY).catch(() => {});
|
||||
AsyncStorage.removeItem('aria_chat_last_sync').catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
// chat_history_response: verpasste Nachrichten nachladen (bei Reconnect)
|
||||
if (message.type === 'chat_history_response') {
|
||||
const p = (message.payload || {}) as any;
|
||||
const incoming = (p.messages || []) as Array<any>;
|
||||
if (!incoming.length) return;
|
||||
console.log(`[Chat] ${incoming.length} verpasste Nachrichten nachgeladen`);
|
||||
const toAdd: ChatMessage[] = incoming.map(m => {
|
||||
const role = m.role === 'user' ? 'user' : 'aria';
|
||||
// ARIA-File-Marker aus dem Backup als attachments rekonstruieren
|
||||
const files = Array.isArray(m.files) ? m.files : [];
|
||||
const attachments = files.map((f: any) => ({
|
||||
type: (typeof f.mimeType === 'string' && f.mimeType.startsWith('image/')) ? 'image' : 'file',
|
||||
name: f.name || 'datei',
|
||||
size: f.size || 0,
|
||||
mimeType: f.mimeType || '',
|
||||
serverPath: f.serverPath || '',
|
||||
})) as Attachment[];
|
||||
return {
|
||||
id: nextId(),
|
||||
sender: role as 'user' | 'aria',
|
||||
text: m.text || '',
|
||||
timestamp: m.ts || Date.now(),
|
||||
attachments: attachments.length ? attachments : undefined,
|
||||
};
|
||||
});
|
||||
const maxTs = incoming.reduce((mx: number, m: any) => Math.max(mx, m.ts || 0), 0);
|
||||
setMessages(prev => {
|
||||
// Dedup auf ts-basis: nicht erneut adden wenn schon was bei +/- 1s vorhanden
|
||||
const existingTs = new Set(prev.map(m => m.timestamp));
|
||||
const newOnes = toAdd.filter(m => !existingTs.has(m.timestamp));
|
||||
return capMessages([...prev, ...newOnes]);
|
||||
});
|
||||
if (maxTs > 0) AsyncStorage.setItem('aria_chat_last_sync', String(maxTs)).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'skill_created') {
|
||||
const p = (message.payload || {}) as any;
|
||||
const skillMsg: ChatMessage = {
|
||||
@@ -480,6 +526,13 @@ const ChatScreen: React.FC = () => {
|
||||
const dbgText = ((message.payload.text as string) || '').slice(0, 60);
|
||||
console.log('[Chat] chat-event sender=%s text=%s', sender || '(none)', dbgText);
|
||||
|
||||
// last-sync tracken — so dass beim Reconnect nicht wieder dieselbe
|
||||
// Nachricht aus dem Server-Backup nachgeladen wird
|
||||
if (sender === 'aria' || sender === 'user' || sender === 'stt') {
|
||||
const ts = message.timestamp || Date.now();
|
||||
AsyncStorage.setItem('aria_chat_last_sync', String(ts)).catch(() => {});
|
||||
}
|
||||
|
||||
// STT-Ergebnis: Transkribierten Text in die Sprach-Bubble schreiben.
|
||||
// WICHTIG: Nur die ERSTE noch unaufgeloeste Aufnahme matchen — sonst
|
||||
// wuerde bei zwei kurz hintereinander gesendeten Audios beide Bubbles
|
||||
@@ -647,6 +700,15 @@ const ChatScreen: React.FC = () => {
|
||||
|
||||
const unsubState = rvs.onStateChange((state) => {
|
||||
setConnectionState(state);
|
||||
// Bei (re)connect: verpasste Chat-Eintraege seit der letzten gesehenen
|
||||
// Nachricht abholen. lastChatSync wird beim Eingang von Nachrichten
|
||||
// hochgezaehlt; default 0 = alle (gecappt auf Server-Limit).
|
||||
if (state === 'connected') {
|
||||
AsyncStorage.getItem('aria_chat_last_sync').then(stored => {
|
||||
const since = stored ? parseInt(stored, 10) || 0 : 0;
|
||||
rvs.send('chat_history_request' as any, { since, limit: 100 });
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
// Initalen Status setzen
|
||||
|
||||
Reference in New Issue
Block a user