Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f862ce1f4 | ||
|
|
528fe97b59 | ||
|
|
3483d1bfce | ||
|
|
158423c155 | ||
|
|
087e91dca1 | ||
|
|
2de4cbc00f | ||
|
|
03fc465057 | ||
|
|
b696b47feb | ||
|
|
6aae565541 | ||
|
|
214bd218a0 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10100
|
||||
versionName "0.1.1.0"
|
||||
versionCode 10104
|
||||
versionName "0.1.1.4"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
+18
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.1.0",
|
||||
"version": "0.1.1.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
@@ -10,31 +10,32 @@
|
||||
"build:apk": "cd android && ./gradlew assembleRelease"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.21.0",
|
||||
"@react-native-community/geolocation": "^3.2.1",
|
||||
"@react-navigation/bottom-tabs": "^6.5.11",
|
||||
"@react-navigation/native": "^6.1.9",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.73.4",
|
||||
"@react-navigation/native": "^6.1.9",
|
||||
"@react-navigation/bottom-tabs": "^6.5.11",
|
||||
"react-native-screens": "3.27.0",
|
||||
"react-native-safe-area-context": "^4.8.2",
|
||||
"react-native-audio-recorder-player": "^3.6.7",
|
||||
"react-native-camera-kit": "^13.0.0",
|
||||
"react-native-document-picker": "^9.1.1",
|
||||
"react-native-sound": "^0.11.2",
|
||||
"@react-native-community/geolocation": "^3.2.1",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-image-picker": "^7.1.0",
|
||||
"react-native-permissions": "^4.1.4",
|
||||
"react-native-camera-kit": "^13.0.0",
|
||||
"@react-native-async-storage/async-storage": "^1.21.0",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-audio-recorder-player": "^3.6.7"
|
||||
"react-native-safe-area-context": "^4.8.2",
|
||||
"react-native-screens": "3.27.0",
|
||||
"react-native-sound": "^0.11.2",
|
||||
"react-native-svg": "^14.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"@react-native/eslint-config": "^0.73.2",
|
||||
"@react-native/metro-config": "^0.73.5",
|
||||
"@react-native/typescript-config": "^0.73.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-native": "^0.73.0",
|
||||
"@react-native/eslint-config": "^0.73.2",
|
||||
"@react-native/typescript-config": "^0.73.1",
|
||||
"@react-native/metro-config": "^0.73.5",
|
||||
"metro-react-native-babel-preset": "^0.77.0",
|
||||
"jest": "^29.7.0",
|
||||
"@types/jest": "^29.5.11"
|
||||
"metro-react-native-babel-preset": "^0.77.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,87 @@
|
||||
/**
|
||||
* MessageText — selektierbarer Chat-Text mit Android-Auto-Linkifizierung.
|
||||
* MessageText — selektierbarer Chat-Text mit Android-Auto-Linkifizierung,
|
||||
* plus Inline-Image-Rendering wenn der Text Bild-URLs enthaelt.
|
||||
*
|
||||
* Wir nutzen Androids dataDetectorType="all" (System macht Phone/URL/Email
|
||||
* automatisch klickbar) und ein einzelnes <Text selectable> ohne nested
|
||||
* <Text> mit eigenem onPress. Nested Text mit onPress fingen die Long-Press-
|
||||
* Geste ab, damit war Markieren+Kopieren defekt.
|
||||
* - Markdown-Syntax `` und plain `https://...image.png` werden
|
||||
* erkannt — die URL bleibt im Text sichtbar (klickbar via Linkify),
|
||||
* zusaetzlich wird das Bild als <Image> oder <SvgUri> drunter gerendert.
|
||||
* - Wir nutzen Androids dataDetectorType="all" (System macht Phone/URL/Email
|
||||
* automatisch klickbar) und ein einzelnes <Text selectable> ohne nested
|
||||
* <Text> mit eigenem onPress — Nested Text mit onPress fing die Long-Press-
|
||||
* Geste ab, damit war Markieren+Kopieren defekt.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Text, TextStyle, StyleProp } from 'react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, Image, TextStyle, StyleProp } from 'react-native';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
const MessageText: React.FC<Props> = ({ text, style }) => {
|
||||
// Bild-URL-Pattern: http(s)://... endend auf gaengige Bild-Endungen.
|
||||
const IMG_URL_RE = /https?:\/\/[^\s)<"']+\.(?:jpe?g|png|gif|webp|bmp|ico|svg)(?:\?[^\s)<"']*)?/gi;
|
||||
|
||||
function extractImageUrls(text: string): string[] {
|
||||
const urls = new Set<string>();
|
||||
const matches = text.match(IMG_URL_RE);
|
||||
if (matches) matches.forEach(u => urls.add(u));
|
||||
return Array.from(urls);
|
||||
}
|
||||
|
||||
const SVG_RE = /\.svg(?:\?|$)/i;
|
||||
|
||||
/** Image mit dynamischer Aspect-Ratio aus echten Bilddimensionen.
|
||||
* SVGs werden ueber react-native-svg gerendert (kein Image.getSize). */
|
||||
const InlineImage: React.FC<{ uri: string }> = ({ uri }) => {
|
||||
const isSvg = SVG_RE.test(uri);
|
||||
const [aspectRatio, setAspectRatio] = useState<number>(1);
|
||||
const [failed, setFailed] = useState(false);
|
||||
useEffect(() => {
|
||||
if (isSvg) return; // Image.getSize geht fuer SVG nicht
|
||||
let cancelled = false;
|
||||
Image.getSize(
|
||||
uri,
|
||||
(w, h) => { if (!cancelled && w > 0 && h > 0) setAspectRatio(Math.max(0.5, Math.min(2.5, w / h))); },
|
||||
() => { if (!cancelled) setFailed(true); },
|
||||
);
|
||||
return () => { cancelled = true; };
|
||||
}, [uri, isSvg]);
|
||||
if (failed) return null;
|
||||
if (isSvg) {
|
||||
return (
|
||||
<View style={{ marginTop: 8, width: 260, height: 260, backgroundColor: '#0D0D1A', borderRadius: 8, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<SvgUri uri={uri} width="100%" height="100%" onError={() => setFailed(true)} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text style={style} selectable dataDetectorType="all">
|
||||
{text}
|
||||
</Text>
|
||||
<Image
|
||||
source={{ uri }}
|
||||
style={{ width: 260, aspectRatio, borderRadius: 8, marginTop: 8, backgroundColor: '#0D0D1A' }}
|
||||
resizeMode="cover"
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MessageText: React.FC<Props> = ({ text, style }) => {
|
||||
const imageUrls = extractImageUrls(text || '');
|
||||
if (imageUrls.length === 0) {
|
||||
return (
|
||||
<Text style={style} selectable dataDetectorType="all">
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<Text style={style} selectable dataDetectorType="all">
|
||||
{text}
|
||||
</Text>
|
||||
{imageUrls.map(u => <InlineImage key={u} uri={u} />)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import RNFS from 'react-native-fs';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||
import audioService from '../services/audio';
|
||||
import wakeWordService from '../services/wakeword';
|
||||
@@ -113,7 +114,9 @@ const ChatImage: React.FC<{
|
||||
onError: () => void;
|
||||
}> = ({ uri, onPress, onError }) => {
|
||||
const [aspectRatio, setAspectRatio] = useState<number>(4 / 3);
|
||||
const isSvg = /\.svg(?:\?|$)/i.test(uri);
|
||||
useEffect(() => {
|
||||
if (isSvg) return; // SvgUri hat kein getSize
|
||||
let cancelled = false;
|
||||
Image.getSize(uri, (w, h) => {
|
||||
if (!cancelled && w > 0 && h > 0) {
|
||||
@@ -124,7 +127,16 @@ const ChatImage: React.FC<{
|
||||
}
|
||||
}, () => {});
|
||||
return () => { cancelled = true; };
|
||||
}, [uri]);
|
||||
}, [uri, isSvg]);
|
||||
if (isSvg) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
|
||||
<View style={[CHAT_IMAGE_STYLE, { height: 260, alignItems: 'center', justifyContent: 'center' }]}>
|
||||
<SvgUri uri={uri} width="100%" height="100%" onError={onError} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
|
||||
<Image
|
||||
@@ -1237,9 +1249,14 @@ const ChatScreen: React.FC = () => {
|
||||
? '\u270D\uFE0F ARIA schreibt...'
|
||||
: '\uD83D\uDCAD ARIA denkt...'}
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.thinkingCancel} onPress={cancelRequest}>
|
||||
<Text style={styles.thinkingCancelText}>Abbrechen</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={{flexDirection: 'row', gap: 6}}>
|
||||
<TouchableOpacity style={[styles.thinkingCancel, {borderColor: '#FF9500'}]} onPress={() => rvs.send('doctor_fix' as any, {})}>
|
||||
<Text style={[styles.thinkingCancelText, {color: '#FF9500'}]}>{'🔧 Reparieren'}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.thinkingCancel} onPress={cancelRequest}>
|
||||
<Text style={styles.thinkingCancelText}>Abbrechen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1351,11 +1368,17 @@ const ChatScreen: React.FC = () => {
|
||||
onPress={() => setFullscreenImage(null)}
|
||||
>
|
||||
{fullscreenImage && (
|
||||
<Image
|
||||
source={{ uri: fullscreenImage }}
|
||||
style={styles.fullscreenImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
/\.svg(?:\?|$)/i.test(fullscreenImage) ? (
|
||||
<View style={styles.fullscreenImage}>
|
||||
<SvgUri uri={fullscreenImage} width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
|
||||
</View>
|
||||
) : (
|
||||
<Image
|
||||
source={{ uri: fullscreenImage }}
|
||||
style={styles.fullscreenImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
@@ -1288,6 +1288,26 @@ const SettingsScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* === ARIA Reparatur === */}
|
||||
<Text style={[styles.sectionTitle, {marginTop: 16}]}>Reparatur</Text>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.toggleHint}>
|
||||
Wenn ARIA gar nicht mehr antwortet oder auf jede Anfrage mit
|
||||
"Antwort ohne Text" zurueckkommt — meistens ein steckengebliebener
|
||||
Run im aria-core. Dieser Button fuehrt {'“'}openclaw doctor --fix{'”'}
|
||||
aus und macht ARIA wieder ansprechbar.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,149,0,0.15)'}]}
|
||||
onPress={() => {
|
||||
rvs.send('doctor_fix' as any, {});
|
||||
ToastAndroid.show('Reparatur-Befehl gesendet — Antwort kommt gleich', ToastAndroid.SHORT);
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.clearButtonText, {color: '#FF9500'}]}>{'🔧 ARIA reparieren'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</>)}
|
||||
|
||||
{/* === Logs === */}
|
||||
|
||||
@@ -132,6 +132,35 @@ Der Marker wird automatisch aus dem sichtbaren Text entfernt und
|
||||
als Anhang-Bubble angezeigt. Stefan tippt drauf → oeffnet die Datei
|
||||
im jeweiligen Standard-Programm.
|
||||
|
||||
### Externe Bilder/Dateien — IMMER runterladen, nicht nur verlinken
|
||||
|
||||
Wenn Stefan ein Bild oder eine Datei aus dem Netz haben will (Wikipedia,
|
||||
Wiki Commons, ein Beispiel-PDF, etc.):
|
||||
|
||||
NICHT NUR die URL in die Antwort schreiben — das Bild ist dann nur
|
||||
solange sichtbar wie der externe Server lebt.
|
||||
|
||||
STATTDESSEN:
|
||||
1. Mit `Bash` + curl/wget herunterladen nach `/shared/uploads/aria_<name>.<ext>`
|
||||
2. Mit `[FILE: ...]`-Marker als Anhang ausspielen
|
||||
|
||||
Beispiel — User: "Zeig mir ein Bild von Micky Maus"
|
||||
|
||||
```bash
|
||||
curl -sL "https://upload.wikimedia.org/wikipedia/commons/7/7f/Mickey_Mouse.svg" \
|
||||
-o /shared/uploads/aria_mickey_mouse.svg
|
||||
```
|
||||
|
||||
Antwort:
|
||||
```
|
||||
Hier Micky Maus — offizielles SVG von Wikimedia Commons (Public Domain).
|
||||
|
||||
[FILE: /shared/uploads/aria_mickey_mouse.svg]
|
||||
```
|
||||
|
||||
So bleibt das Bild permanent im Chat-Verlauf, auch wenn die Wiki-URL
|
||||
spaeter offline geht oder umgezogen wird.
|
||||
|
||||
## Stimme
|
||||
|
||||
TTS laeuft ueber F5-TTS auf der Gamebox (Voice Cloning). Stefan kann
|
||||
|
||||
@@ -1580,6 +1580,43 @@ class ARIABridge:
|
||||
except Exception as e:
|
||||
logger.warning("[rvs] file_saved konnte nicht an App gesendet werden: %s", e)
|
||||
|
||||
elif msg_type == "doctor_fix":
|
||||
# App-Button "ARIA reparieren" → openclaw doctor --fix anstossen.
|
||||
# Bridge erreicht aria-core nicht via docker (kein docker-socket
|
||||
# gemountet), aber der Diagnostic-Server hat den Socket. HTTP-Call
|
||||
# an http://localhost:3001/api/doctor-fix.
|
||||
logger.info("[rvs] doctor_fix Request von App — leite an Diagnostic weiter")
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
"http://localhost:3001/api/doctor-fix",
|
||||
data=b"{}",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
# Blocking call ist OK weil openclaw doctor schnell durchlaeuft.
|
||||
# In Executor laufen lassen damit der asyncio-Loop nicht blockt.
|
||||
def _do_fix():
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return resp.status, resp.read().decode("utf-8", errors="ignore")
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_fix)
|
||||
ok = status == 200
|
||||
logger.info("[rvs] doctor_fix Result: status=%s ok=%s", status, ok)
|
||||
await self._send_to_rvs({
|
||||
"type": "chat",
|
||||
"payload": {
|
||||
"text": "[Reparatur] ARIA wurde durchgecheckt — sollte wieder antworten." if ok
|
||||
else f"[Reparatur] Fehlgeschlagen: {body[:200]}",
|
||||
"sender": "aria",
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning("[rvs] doctor_fix Weiterleitung fehlgeschlagen: %s", e)
|
||||
return
|
||||
|
||||
elif msg_type == "file_request":
|
||||
# App fordert eine Datei an (Re-Download nach Cache-Leerung)
|
||||
server_path = payload.get("serverPath", "")
|
||||
|
||||
+18
-1
@@ -288,7 +288,10 @@
|
||||
<div class="chat-box" id="chat-box"></div>
|
||||
<div id="thinking-indicator" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:0 0 6px 6px;margin-top:-8px;margin-bottom:8px;align-items:center;justify-content:space-between;">
|
||||
<span><span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text">ARIA denkt...</span></span>
|
||||
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<button class="btn secondary" onclick="doctorFix()" style="padding:2px 10px;font-size:11px;color:#FF9500;border-color:#FF9500;" title="ARIA reparieren — openclaw doctor --fix">🔧 Reparieren</button>
|
||||
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="diag-pending-attachments" style="display:none;padding:6px 10px;background:#1E1E2E;border-radius:6px 6px 0 0;margin-bottom:-4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
|
||||
</div>
|
||||
@@ -1858,6 +1861,20 @@
|
||||
renderDiagPending();
|
||||
}
|
||||
|
||||
// ── Reparieren — openclaw doctor --fix ──────
|
||||
function doctorFix() {
|
||||
fetch('/api/doctor-fix', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
addLog('info', 'server', 'Reparatur ausgefuehrt: ' + (data.output || 'OK').slice(0, 200));
|
||||
} else {
|
||||
addLog('error', 'server', 'Reparatur fehlgeschlagen: ' + (data.error || ''));
|
||||
}
|
||||
})
|
||||
.catch(err => addLog('error', 'server', 'Reparatur Request fehlgeschlagen: ' + err.message));
|
||||
}
|
||||
|
||||
// ── Abbrechen ──────────────────────────────
|
||||
function cancelRequest() {
|
||||
send({ action: 'cancel_request' });
|
||||
|
||||
@@ -1342,6 +1342,22 @@ const server = http.createServer((req, res) => {
|
||||
dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} else if (req.url === "/api/doctor-fix" && req.method === "POST") {
|
||||
// Manueller "ARIA reparieren"-Button — stuck OpenClaw-Runs aufloesen.
|
||||
log("info", "server", "HTTP /api/doctor-fix — manueller Reparatur-Trigger");
|
||||
dockerExec("aria-core", "openclaw doctor --fix 2>&1")
|
||||
.then(out => {
|
||||
const summary = (out || "").split("\n").filter(l => l.trim()).slice(-3).join(" | ");
|
||||
broadcast({ type: "watchdog", status: "fixed", message: `Reparatur ausgefuehrt: ${summary || "OK"}` });
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, output: out }));
|
||||
})
|
||||
.catch(err => {
|
||||
broadcast({ type: "watchdog", status: "error", message: `Reparatur fehlgeschlagen: ${err.message}` });
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||
});
|
||||
return;
|
||||
} else if (req.url.startsWith("/shared/")) {
|
||||
// Dateien aus Shared Volume ausliefern (Bilder, Uploads)
|
||||
const filePath = decodeURIComponent(req.url);
|
||||
|
||||
@@ -19,6 +19,7 @@ const ALLOWED_TYPES = new Set([
|
||||
"agent_activity", "cancel_request",
|
||||
"audio_pcm",
|
||||
"file_from_aria",
|
||||
"doctor_fix",
|
||||
"xtts_delete_voice",
|
||||
"voice_preload", "voice_ready",
|
||||
"stt_request", "stt_response",
|
||||
|
||||
Reference in New Issue
Block a user