Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b5591aa72 | ||
|
|
b70623bb78 | ||
|
|
73b6827f6b | ||
|
|
a7998bcd8b |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 20503
|
||||
versionName "0.2.5.3"
|
||||
versionCode 20504
|
||||
versionName "0.2.5.4"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.2.5.3",
|
||||
"version": "0.2.5.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -200,6 +200,28 @@ function formatDur(sec: number): string {
|
||||
return `${m}:${r.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// ─── Grenzen fuer den Anhang-Re-Download ────────────────────────────
|
||||
//
|
||||
// Ein Re-Download kommt als base64 in EINER WebSocket-Nachricht. Die muss die
|
||||
// App komplett im Speicher puffern und dann durch JSON.parse schicken — bei
|
||||
// einem 150-MB-Video sind das ~200 MB base64, und das Handy stirbt daran mit
|
||||
// einem nativen OOM (kein JS-Fehler, darum tauchte nie ein Stacktrace auf).
|
||||
//
|
||||
// Genau das war die Absturzschleife: App startet → Auto-Re-Download fordert
|
||||
// ungefragt das 150-MB-Video an → OOM → Android startet den Vordergrund-
|
||||
// Dienst neu → App startet → von vorn. Alle ~70s, stundenlang.
|
||||
//
|
||||
// Darum zwei Grenzen:
|
||||
// AUTO — was die App OHNE Nachfrage holt. Klein halten; Bilder und
|
||||
// Dokumente passen rein, Videos nicht.
|
||||
// TAP — was sie auf ausdruecklichen Tipp holt. Hoeher, aber immer noch
|
||||
// weit unter der Schmerzgrenze. Drueber gibt's eine ehrliche
|
||||
// Meldung statt eines Absturzes.
|
||||
const AUTO_DL_MAX_BYTES = 25 * 1024 * 1024;
|
||||
const TAP_DL_MAX_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
const fmtMB = (n: number) => `${(n / 1024 / 1024).toFixed(0)} MB`;
|
||||
|
||||
const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
|
||||
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
||||
|
||||
@@ -425,6 +447,33 @@ const ChatScreen: React.FC = () => {
|
||||
// Intent geoeffnet (PDF-Viewer, Galerie, etc.).
|
||||
const autoOpenPaths = useRef<Set<string>>(new Set());
|
||||
|
||||
/**
|
||||
* Re-Download anfordern — mit Groessen-Bremse.
|
||||
* Gibt false zurueck (und sagt warum), wenn die Datei zu gross fuer einen
|
||||
* Transfer durch die WebSocket ist. Ohne das holt sich die App ein 150-MB-
|
||||
* Video als ~200 MB base64 und stirbt am nativen OOM (siehe TAP_DL_MAX_BYTES).
|
||||
*/
|
||||
const requestAttachment = useCallback((att: Attachment, reqId: string): boolean => {
|
||||
if (!att.serverPath) {
|
||||
if (Platform.OS === 'android') {
|
||||
ToastAndroid.show('Datei kann nicht nachgeladen werden (kein serverPath)', ToastAndroid.LONG);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (typeof att.size === 'number' && att.size > TAP_DL_MAX_BYTES) {
|
||||
if (Platform.OS === 'android') {
|
||||
ToastAndroid.show(
|
||||
`${att.name}: ${fmtMB(att.size)} — zu gross fuers Handy. `
|
||||
+ `Liegt auf dem Server unter ${att.serverPath}`,
|
||||
ToastAndroid.LONG,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: reqId });
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
// Eindeutige Message-ID generieren
|
||||
const nextId = (): string => {
|
||||
messageIdCounter.current += 1;
|
||||
@@ -860,13 +909,26 @@ const ChatScreen: React.FC = () => {
|
||||
setTimeout(() => {
|
||||
setMessages(prev => {
|
||||
const missing: {id: string, serverPath: string}[] = [];
|
||||
const skipped: string[] = [];
|
||||
for (const msg of prev) {
|
||||
for (const att of msg.attachments || []) {
|
||||
if (att.serverPath && !att.uri) {
|
||||
missing.push({ id: msg.id, serverPath: att.serverPath });
|
||||
if (!att.serverPath || att.uri) continue;
|
||||
// Grosse Dateien NICHT ungefragt holen (siehe AUTO_DL_MAX_BYTES).
|
||||
// Sie bleiben als "tippen zum Laden" stehen — der User
|
||||
// entscheidet, ob er sie wirklich aufs Handy zieht.
|
||||
if (typeof att.size === 'number' && att.size > AUTO_DL_MAX_BYTES) {
|
||||
skipped.push(`${att.name} (${fmtMB(att.size)})`);
|
||||
continue;
|
||||
}
|
||||
missing.push({ id: msg.id, serverPath: att.serverPath });
|
||||
}
|
||||
}
|
||||
if (skipped.length > 0) {
|
||||
console.log('[Chat] Auto-Download uebersprungen (zu gross): %s', skipped.join(', '));
|
||||
import('../services/logger')
|
||||
.then(m => m.reportAppDebug('chat.autodl', `uebersprungen: ${skipped.join(', ')}`))
|
||||
.catch(() => {});
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
console.log(`[Chat] ${missing.length} fehlende Anhaenge — lade nach...`);
|
||||
for (const m of missing) {
|
||||
@@ -2869,16 +2931,15 @@ const ChatScreen: React.FC = () => {
|
||||
) : att.type === 'image' && !att.uri ? (
|
||||
<TouchableOpacity
|
||||
style={styles.attachmentFile}
|
||||
onPress={() => {
|
||||
if (att.serverPath) {
|
||||
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id });
|
||||
}
|
||||
}}
|
||||
onPress={() => { requestAttachment(att, item.id); }}
|
||||
>
|
||||
<Text style={styles.attachmentFileIcon}>{'\uD83D\uDDBC\uFE0F'}</Text>
|
||||
<Text style={styles.attachmentFileName} numberOfLines={1}>{att.name}</Text>
|
||||
<Text style={styles.attachmentFileSize}>
|
||||
{att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'}
|
||||
{!att.serverPath ? '(nicht verfuegbar)'
|
||||
: typeof att.size === 'number' && att.size > AUTO_DL_MAX_BYTES
|
||||
? `(${fmtMB(att.size)} — tippen zum Laden)`
|
||||
: '(tippen zum Laden)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : att.type === 'audio' ? (
|
||||
@@ -2915,11 +2976,9 @@ const ChatScreen: React.FC = () => {
|
||||
}
|
||||
// Re-Download via file_request \u2192 bei file_response wird die
|
||||
// Datei gespeichert UND geoeffnet (autoOpenPaths-Tracking).
|
||||
if (att.serverPath) {
|
||||
autoOpenPaths.current.add(att.serverPath);
|
||||
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id });
|
||||
} else if (Platform.OS === 'android') {
|
||||
ToastAndroid.show('Datei kann nicht nachgeladen werden (kein serverPath)', ToastAndroid.LONG);
|
||||
if (att.serverPath) autoOpenPaths.current.add(att.serverPath);
|
||||
if (!requestAttachment(att, item.id) && att.serverPath) {
|
||||
autoOpenPaths.current.delete(att.serverPath);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -49,8 +49,15 @@ const BREADCRUMB_KEY = 'aria_last_breadcrumb';
|
||||
let _breadcrumb: { ts: number; scope: string; message: string } = { ts: 0, scope: '', message: '' };
|
||||
let _breadcrumbDirty = false;
|
||||
|
||||
/** Letzte App-Aktivitaet merken — Crash-Kontext fuer den naechsten Boot. */
|
||||
/** Letzte App-Aktivitaet merken — Crash-Kontext fuer den naechsten Boot.
|
||||
*
|
||||
* Die Crash-Meldung selbst darf den Brotkrumen NICHT ueberschreiben: sonst
|
||||
* merkt sich der zweite Absturz nur noch "letzte Aktivitaet: Crash-Meldung",
|
||||
* der dritte "letzte Aktivitaet: Crash-Meldung ueber Crash-Meldung" — und bei
|
||||
* einer Absturzschleife ist nach dem ersten Durchlauf jede Spur weg. Genau
|
||||
* das ist passiert; die eigentliche Ursache stand nur in der ERSTEN Meldung. */
|
||||
export function noteBreadcrumb(scope: string, message: string): void {
|
||||
if (scope === 'app.crash-detected') return;
|
||||
_breadcrumb = { ts: Date.now(), scope: scope || '', message: String(message || '').slice(0, 120) };
|
||||
_breadcrumbDirty = true;
|
||||
}
|
||||
|
||||
+20
-6
@@ -3550,6 +3550,10 @@ class ARIABridge:
|
||||
elif msg_type == "sat_hello":
|
||||
sid = (payload.get("id") or "").strip()
|
||||
if sid:
|
||||
# sat_hello kommt alle ~25s erneut. Nur Neuanmeldungen und echte
|
||||
# Aenderungen loggen — sonst ersaeuft das Bridge-Log in
|
||||
# "Satellit online", und die interessanten Zeilen sind weg.
|
||||
prev = self._satellites.get(sid)
|
||||
self._satellites[sid] = {
|
||||
"id": sid,
|
||||
"location": payload.get("location") or sid,
|
||||
@@ -3557,9 +3561,12 @@ class ARIABridge:
|
||||
"control": bool(payload.get("control")),
|
||||
"last_seen": time.time(),
|
||||
}
|
||||
logger.info("[sat] Satellit online: %s (%s) caps=%s control=%s",
|
||||
sid, self._satellites[sid]["location"],
|
||||
self._satellites[sid]["caps"], self._satellites[sid]["control"])
|
||||
cur = self._satellites[sid]
|
||||
if prev is None or any(prev.get(k) != cur.get(k)
|
||||
for k in ("location", "caps", "control")):
|
||||
logger.info("[sat] Satellit %s: %s (%s) caps=%s control=%s",
|
||||
"online" if prev is None else "aktualisiert",
|
||||
sid, cur["location"], cur["caps"], cur["control"])
|
||||
return
|
||||
|
||||
elif msg_type == "host_hello":
|
||||
@@ -3625,9 +3632,16 @@ class ARIABridge:
|
||||
"busy": bool(prev.get("busy", False)),
|
||||
"last_seen": time.time(),
|
||||
}
|
||||
logger.info("[worker] online: %s (service=%s node=%s gpus=%s model=%s)",
|
||||
iid, self._workers[iid]["service"], self._workers[iid]["node"],
|
||||
self._workers[iid]["gpus"] or "?", self._workers[iid]["model"] or "?")
|
||||
# Wie bei sat_hello: worker_hello wiederholt sich im Takt von
|
||||
# ~30s. Nur neue Boxen und echte Aenderungen (anderes Modell,
|
||||
# andere GPUs) ins Log — der Rest ist Rauschen.
|
||||
cur = self._workers[iid]
|
||||
if not prev or any(prev.get(k) != cur.get(k)
|
||||
for k in ("service", "node", "gpus", "model", "models")):
|
||||
logger.info("[worker] %s: %s (service=%s node=%s gpus=%s model=%s)",
|
||||
"online" if not prev else "aktualisiert",
|
||||
iid, cur["service"], cur["node"],
|
||||
cur["gpus"] or "?", cur["model"] or "?")
|
||||
return
|
||||
|
||||
elif msg_type == "stt_lease_request":
|
||||
|
||||
+106
@@ -124,6 +124,86 @@ const ALLOWED_TYPES = new Set([
|
||||
// Token-Raum: token -> { clients: Set<ws> }
|
||||
const rooms = new Map();
|
||||
|
||||
// ── Punkt-zu-Punkt-Antworten ────────────────────────────────────────
|
||||
//
|
||||
// Der RVS ist ein Broadcast-Relay: normalerweise bekommt JEDER Client im Raum
|
||||
// jede Nachricht. Bei Datei-Antworten ist das fatal. Ein 150-MB-Video wird als
|
||||
// base64 ~200 MB gross und ging bisher an ALLE Clients im Raum — auch an die
|
||||
// STT/TTS-Worker, die daran mit Code 1009 (message too big) gestorben sind und
|
||||
// die gerade laufende Sprachaufnahme mitgerissen haben ("erste Sprachnachricht
|
||||
// kommt nicht durch"). Die Worker-max_size-Angleichung verhindert den Absturz,
|
||||
// aber 200 MB an zehn Clients zu schicken bleibt Unsinn: nur EINER hat gefragt.
|
||||
//
|
||||
// Darum: wer eine Anfrage mit requestId stellt, wird gemerkt — die Antwort mit
|
||||
// derselben requestId geht nur an ihn zurueck. Unbekannte requestId → wie
|
||||
// bisher broadcasten (nichts geht verloren, falls ein Client mithoeren muss,
|
||||
// dessen Anfrage der RVS nie gesehen hat).
|
||||
const TARGETED = new Map([
|
||||
["file_request", "file_response"],
|
||||
["file_zip_request", "file_zip_response"],
|
||||
["file_version_download_request", "file_version_download_response"],
|
||||
["brain_request", "brain_response"],
|
||||
]);
|
||||
const TARGETED_REPLIES = new Set(TARGETED.values());
|
||||
|
||||
// `antwortTyp \0 requestId` -> { clients: Set<ws>, at }
|
||||
// Mehrere Clients koennen dieselbe requestId benutzen (die App nimmt die
|
||||
// Nachrichten-ID) — darum ein Set, sonst bekaeme nur der letzte die Antwort.
|
||||
const pendingReplies = new Map();
|
||||
const PENDING_TTL_MS = 180_000;
|
||||
|
||||
function pendingKey(replyType, requestId) {
|
||||
return `${replyType} | ||||