Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b5591aa72 | ||
|
|
b70623bb78 | ||
|
|
73b6827f6b | ||
|
|
a7998bcd8b |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 20503
|
versionCode 20504
|
||||||
versionName "0.2.5.3"
|
versionName "0.2.5.4"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.2.5.3",
|
"version": "0.2.5.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -200,6 +200,28 @@ function formatDur(sec: number): string {
|
|||||||
return `${m}:${r.toString().padStart(2, '0')}`;
|
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 DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
|
||||||
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
||||||
|
|
||||||
@@ -425,6 +447,33 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Intent geoeffnet (PDF-Viewer, Galerie, etc.).
|
// Intent geoeffnet (PDF-Viewer, Galerie, etc.).
|
||||||
const autoOpenPaths = useRef<Set<string>>(new Set());
|
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
|
// Eindeutige Message-ID generieren
|
||||||
const nextId = (): string => {
|
const nextId = (): string => {
|
||||||
messageIdCounter.current += 1;
|
messageIdCounter.current += 1;
|
||||||
@@ -860,13 +909,26 @@ const ChatScreen: React.FC = () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setMessages(prev => {
|
setMessages(prev => {
|
||||||
const missing: {id: string, serverPath: string}[] = [];
|
const missing: {id: string, serverPath: string}[] = [];
|
||||||
|
const skipped: string[] = [];
|
||||||
for (const msg of prev) {
|
for (const msg of prev) {
|
||||||
for (const att of msg.attachments || []) {
|
for (const att of msg.attachments || []) {
|
||||||
if (att.serverPath && !att.uri) {
|
if (!att.serverPath || att.uri) continue;
|
||||||
missing.push({ id: msg.id, serverPath: att.serverPath });
|
// 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) {
|
if (missing.length > 0) {
|
||||||
console.log(`[Chat] ${missing.length} fehlende Anhaenge — lade nach...`);
|
console.log(`[Chat] ${missing.length} fehlende Anhaenge — lade nach...`);
|
||||||
for (const m of missing) {
|
for (const m of missing) {
|
||||||
@@ -2869,16 +2931,15 @@ const ChatScreen: React.FC = () => {
|
|||||||
) : att.type === 'image' && !att.uri ? (
|
) : att.type === 'image' && !att.uri ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.attachmentFile}
|
style={styles.attachmentFile}
|
||||||
onPress={() => {
|
onPress={() => { requestAttachment(att, item.id); }}
|
||||||
if (att.serverPath) {
|
|
||||||
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.attachmentFileIcon}>{'\uD83D\uDDBC\uFE0F'}</Text>
|
<Text style={styles.attachmentFileIcon}>{'\uD83D\uDDBC\uFE0F'}</Text>
|
||||||
<Text style={styles.attachmentFileName} numberOfLines={1}>{att.name}</Text>
|
<Text style={styles.attachmentFileName} numberOfLines={1}>{att.name}</Text>
|
||||||
<Text style={styles.attachmentFileSize}>
|
<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>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : att.type === 'audio' ? (
|
) : att.type === 'audio' ? (
|
||||||
@@ -2915,11 +2976,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
// Re-Download via file_request \u2192 bei file_response wird die
|
// Re-Download via file_request \u2192 bei file_response wird die
|
||||||
// Datei gespeichert UND geoeffnet (autoOpenPaths-Tracking).
|
// Datei gespeichert UND geoeffnet (autoOpenPaths-Tracking).
|
||||||
if (att.serverPath) {
|
if (att.serverPath) autoOpenPaths.current.add(att.serverPath);
|
||||||
autoOpenPaths.current.add(att.serverPath);
|
if (!requestAttachment(att, item.id) && att.serverPath) {
|
||||||
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id });
|
autoOpenPaths.current.delete(att.serverPath);
|
||||||
} else if (Platform.OS === 'android') {
|
|
||||||
ToastAndroid.show('Datei kann nicht nachgeladen werden (kein serverPath)', ToastAndroid.LONG);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,8 +49,15 @@ const BREADCRUMB_KEY = 'aria_last_breadcrumb';
|
|||||||
let _breadcrumb: { ts: number; scope: string; message: string } = { ts: 0, scope: '', message: '' };
|
let _breadcrumb: { ts: number; scope: string; message: string } = { ts: 0, scope: '', message: '' };
|
||||||
let _breadcrumbDirty = false;
|
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 {
|
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) };
|
_breadcrumb = { ts: Date.now(), scope: scope || '', message: String(message || '').slice(0, 120) };
|
||||||
_breadcrumbDirty = true;
|
_breadcrumbDirty = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-6
@@ -3550,6 +3550,10 @@ class ARIABridge:
|
|||||||
elif msg_type == "sat_hello":
|
elif msg_type == "sat_hello":
|
||||||
sid = (payload.get("id") or "").strip()
|
sid = (payload.get("id") or "").strip()
|
||||||
if sid:
|
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] = {
|
self._satellites[sid] = {
|
||||||
"id": sid,
|
"id": sid,
|
||||||
"location": payload.get("location") or sid,
|
"location": payload.get("location") or sid,
|
||||||
@@ -3557,9 +3561,12 @@ class ARIABridge:
|
|||||||
"control": bool(payload.get("control")),
|
"control": bool(payload.get("control")),
|
||||||
"last_seen": time.time(),
|
"last_seen": time.time(),
|
||||||
}
|
}
|
||||||
logger.info("[sat] Satellit online: %s (%s) caps=%s control=%s",
|
cur = self._satellites[sid]
|
||||||
sid, self._satellites[sid]["location"],
|
if prev is None or any(prev.get(k) != cur.get(k)
|
||||||
self._satellites[sid]["caps"], self._satellites[sid]["control"])
|
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
|
return
|
||||||
|
|
||||||
elif msg_type == "host_hello":
|
elif msg_type == "host_hello":
|
||||||
@@ -3625,9 +3632,16 @@ class ARIABridge:
|
|||||||
"busy": bool(prev.get("busy", False)),
|
"busy": bool(prev.get("busy", False)),
|
||||||
"last_seen": time.time(),
|
"last_seen": time.time(),
|
||||||
}
|
}
|
||||||
logger.info("[worker] online: %s (service=%s node=%s gpus=%s model=%s)",
|
# Wie bei sat_hello: worker_hello wiederholt sich im Takt von
|
||||||
iid, self._workers[iid]["service"], self._workers[iid]["node"],
|
# ~30s. Nur neue Boxen und echte Aenderungen (anderes Modell,
|
||||||
self._workers[iid]["gpus"] or "?", self._workers[iid]["model"] or "?")
|
# 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
|
return
|
||||||
|
|
||||||
elif msg_type == "stt_lease_request":
|
elif msg_type == "stt_lease_request":
|
||||||
|
|||||||
+106
@@ -124,6 +124,86 @@ const ALLOWED_TYPES = new Set([
|
|||||||
// Token-Raum: token -> { clients: Set<ws> }
|
// Token-Raum: token -> { clients: Set<ws> }
|
||||||
const rooms = new Map();
|
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} | ||||||