Compare commits

...

15 Commits

10 changed files with 375 additions and 49 deletions
+2 -2
View File
@@ -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 1 versionCode 109
versionName "0.0.1.7" versionName "0.0.1.9"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.0.1.7", "version": "0.0.1.9",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+33 -3
View File
@@ -90,6 +90,7 @@ const ChatScreen: React.FC = () => {
const [showCameraUpload, setShowCameraUpload] = useState(false); const [showCameraUpload, setShowCameraUpload] = useState(false);
const [gpsEnabled, setGpsEnabled] = useState(false); const [gpsEnabled, setGpsEnabled] = useState(false);
const [wakeWordActive, setWakeWordActive] = useState(false); const [wakeWordActive, setWakeWordActive] = useState(false);
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
const flatListRef = useRef<FlatList>(null); const flatListRef = useRef<FlatList>(null);
const messageIdCounter = useRef(0); const messageIdCounter = useRef(0);
@@ -525,12 +526,12 @@ const ChatScreen: React.FC = () => {
{item.attachments?.map((att, idx) => ( {item.attachments?.map((att, idx) => (
<View key={idx}> <View key={idx}>
{att.type === 'image' && att.uri ? ( {att.type === 'image' && att.uri ? (
<TouchableOpacity onPress={() => setFullscreenImage(att.uri || null)} activeOpacity={0.8}>
<Image <Image
source={{ uri: att.uri }} source={{ uri: att.uri }}
style={styles.attachmentImage} style={styles.attachmentImage}
resizeMode="contain" resizeMode="cover"
onError={() => { onError={() => {
// Bild nicht mehr verfuegbar — Placeholder setzen
setMessages(prev => prev.map(m => setMessages(prev => prev.map(m =>
m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) => m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) =>
i === idx ? { ...a, uri: undefined } : a i === idx ? { ...a, uri: undefined } : a
@@ -538,6 +539,7 @@ const ChatScreen: React.FC = () => {
)); ));
}} }}
/> />
</TouchableOpacity>
) : att.type === 'image' && !att.uri ? ( ) : att.type === 'image' && !att.uri ? (
<TouchableOpacity <TouchableOpacity
style={styles.attachmentFile} style={styles.attachmentFile}
@@ -675,6 +677,23 @@ const ChatScreen: React.FC = () => {
)} )}
</View> </View>
{/* Bild-Vollbild Modal */}
<Modal visible={!!fullscreenImage} transparent animationType="fade" onRequestClose={() => setFullscreenImage(null)}>
<TouchableOpacity
style={styles.fullscreenOverlay}
activeOpacity={1}
onPress={() => setFullscreenImage(null)}
>
{fullscreenImage && (
<Image
source={{ uri: fullscreenImage }}
style={styles.fullscreenImage}
resizeMode="contain"
/>
)}
</TouchableOpacity>
</Modal>
{/* Datei-Upload Modal */} {/* Datei-Upload Modal */}
<Modal visible={showFileUpload} transparent animationType="slide"> <Modal visible={showFileUpload} transparent animationType="slide">
<View style={styles.modalOverlay}> <View style={styles.modalOverlay}>
@@ -757,7 +776,8 @@ const styles = StyleSheet.create({
}, },
attachmentImage: { attachmentImage: {
width: '100%', width: '100%',
height: 200, minHeight: 200,
maxHeight: 400,
borderRadius: 8, borderRadius: 8,
marginBottom: 6, marginBottom: 6,
backgroundColor: '#0D0D1A', backgroundColor: '#0D0D1A',
@@ -867,6 +887,16 @@ const styles = StyleSheet.create({
wakeWordIcon: { wakeWordIcon: {
fontSize: 16, fontSize: 16,
}, },
fullscreenOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.95)',
justifyContent: 'center',
alignItems: 'center',
},
fullscreenImage: {
width: '100%',
height: '100%',
},
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0,0,0,0.6)', backgroundColor: 'rgba(0,0,0,0.6)',
+122 -1
View File
@@ -71,6 +71,9 @@ const SettingsScreen: React.FC = () => {
const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH); const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH);
const [autoDownload, setAutoDownload] = useState(true); const [autoDownload, setAutoDownload] = useState(true);
const [storageSize, setStorageSize] = useState('...'); const [storageSize, setStorageSize] = useState('...');
const [ttsEnabled, setTtsEnabled] = useState(true);
const [defaultVoice, setDefaultVoice] = useState('ramona');
const [highlightVoice, setHighlightVoice] = useState('thorsten');
const [editingPath, setEditingPath] = useState(false); const [editingPath, setEditingPath] = useState(false);
const [tempPath, setTempPath] = useState(''); const [tempPath, setTempPath] = useState('');
@@ -91,6 +94,15 @@ const SettingsScreen: React.FC = () => {
AsyncStorage.getItem('aria_auto_download').then(saved => { AsyncStorage.getItem('aria_auto_download').then(saved => {
if (saved !== null) setAutoDownload(saved === 'true'); if (saved !== null) setAutoDownload(saved === 'true');
}); });
AsyncStorage.getItem('aria_tts_enabled').then(saved => {
if (saved !== null) setTtsEnabled(saved === 'true');
});
AsyncStorage.getItem('aria_default_voice').then(saved => {
if (saved) setDefaultVoice(saved);
});
AsyncStorage.getItem('aria_highlight_voice').then(saved => {
if (saved) setHighlightVoice(saved);
});
}, []); }, []);
// Speichergroesse berechnen // Speichergroesse berechnen
@@ -442,6 +454,83 @@ const SettingsScreen: React.FC = () => {
</View> </View>
</View> </View>
{/* === Sprachausgabe === */}
<Text style={styles.sectionTitle}>Sprachausgabe</Text>
<View style={styles.card}>
{/* TTS An/Aus */}
<View style={styles.toggleRow}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>Sprachausgabe</Text>
<Text style={styles.toggleHint}>ARIA antwortet per Sprache (TTS)</Text>
</View>
<Switch
value={ttsEnabled}
onValueChange={(val) => {
setTtsEnabled(val);
AsyncStorage.setItem('aria_tts_enabled', String(val));
rvs.send('config' as any, { ttsEnabled: val });
}}
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
thumbColor={ttsEnabled ? '#FFFFFF' : '#666680'}
/>
</View>
{/* Standard-Stimme */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Standard-Stimme</Text>
<Text style={styles.toggleHint}>Fuer normale Antworten und Gespraeche</Text>
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
<TouchableOpacity
style={[styles.voiceBtn, defaultVoice === 'ramona' && styles.voiceBtnActive]}
onPress={() => { setDefaultVoice('ramona'); AsyncStorage.setItem('aria_default_voice', 'ramona'); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
<Text style={[styles.voiceBtnText, defaultVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
<Text style={styles.voiceBtnHint}>Weiblich, warm</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.voiceBtn, defaultVoice === 'thorsten' && styles.voiceBtnActive]}
onPress={() => { setDefaultVoice('thorsten'); AsyncStorage.setItem('aria_default_voice', 'thorsten'); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
<Text style={[styles.voiceBtnText, defaultVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
<Text style={styles.voiceBtnHint}>Maennlich, tief</Text>
</TouchableOpacity>
</View>
</View>
{/* Highlight-Stimme */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Highlight-Stimme</Text>
<Text style={styles.toggleHint}>Fuer besondere Ereignisse (Deploy, Alarm, Erfolg)</Text>
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
<TouchableOpacity
style={[styles.voiceBtn, highlightVoice === 'thorsten' && styles.voiceBtnActive]}
onPress={() => { setHighlightVoice('thorsten'); AsyncStorage.setItem('aria_highlight_voice', 'thorsten'); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
<Text style={[styles.voiceBtnText, highlightVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.voiceBtn, highlightVoice === 'ramona' && styles.voiceBtnActive]}
onPress={() => { setHighlightVoice('ramona'); AsyncStorage.setItem('aria_highlight_voice', 'ramona'); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
<Text style={[styles.voiceBtnText, highlightVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
</TouchableOpacity>
</View>
</View>
{/* Highlight-Trigger Info */}
<View style={{marginTop: 16, padding: 10, backgroundColor: '#1E1E2E', borderRadius: 8}}>
<Text style={styles.toggleLabel}>{'\u26A1'} Highlight-Trigger</Text>
<Text style={[styles.toggleHint, {marginTop: 4}]}>
Die Highlight-Stimme wird automatisch bei diesen Woertern verwendet:{'\n'}
deploy, erfolgreich, alarm, so soll es sein, kritisch, server down, sicherheitswarnung, ticket geloest, aufgabe abgeschlossen
</Text>
</View>
</View>
{/* === Speicher === */} {/* === Speicher === */}
<Text style={styles.sectionTitle}>Anhang-Speicher</Text> <Text style={styles.sectionTitle}>Anhang-Speicher</Text>
<View style={styles.card}> <View style={styles.card}>
@@ -601,7 +690,7 @@ const SettingsScreen: React.FC = () => {
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text> <Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.aboutTitle}>ARIA Cockpit</Text> <Text style={styles.aboutTitle}>ARIA Cockpit</Text>
<Text style={styles.aboutVersion}>Version 0.0.1.6 </Text> <Text style={styles.aboutVersion}>Version 0.0.1.9 </Text>
<Text style={styles.aboutInfo}> <Text style={styles.aboutInfo}>
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'} Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
Gebaut mit React Native + TypeScript. Gebaut mit React Native + TypeScript.
@@ -744,6 +833,38 @@ const styles = StyleSheet.create({
marginTop: 2, marginTop: 2,
}, },
// Stimmen
voiceBtn: {
flex: 1,
padding: 12,
borderRadius: 10,
backgroundColor: '#1E1E2E',
alignItems: 'center',
borderWidth: 2,
borderColor: 'transparent',
},
voiceBtnActive: {
borderColor: '#0096FF',
backgroundColor: '#0D1A2E',
},
voiceBtnIcon: {
fontSize: 28,
marginBottom: 4,
},
voiceBtnText: {
color: '#8888AA',
fontSize: 14,
fontWeight: '600',
},
voiceBtnTextActive: {
color: '#FFFFFF',
},
voiceBtnHint: {
color: '#555570',
fontSize: 11,
marginTop: 2,
},
// Speicher // Speicher
storagePathText: { storagePathText: {
color: '#0096FF', color: '#0096FF',
+12 -5
View File
@@ -184,6 +184,9 @@ class VoiceEngine:
tmp_path = tmp.name tmp_path = tmp.name
with wave.open(tmp_path, "wb") as wav_file: with wave.open(tmp_path, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2) # 16-bit
wav_file.setframerate(voice.config.sample_rate)
voice.synthesize(text, wav_file) voice.synthesize(text, wav_file)
audio_data = Path(tmp_path).read_bytes() audio_data = Path(tmp_path).read_bytes()
@@ -927,7 +930,7 @@ class ARIABridge:
if msg_type == "chat": if msg_type == "chat":
# Nur User-Nachrichten weiterleiten — ARIA/Diagnostic-Antworten ignorieren (sonst Loop!) # Nur User-Nachrichten weiterleiten — ARIA/Diagnostic-Antworten ignorieren (sonst Loop!)
sender = payload.get("sender", "") sender = payload.get("sender", "")
if sender in ("aria", "diagnostic", "stt"): if sender in ("aria", "stt"):
return return
text = payload.get("text", "") text = payload.get("text", "")
if text: if text:
@@ -984,7 +987,8 @@ class ARIABridge:
text = (f"Stefan hat dir ein Bild geschickt: {file_name}" text = (f"Stefan hat dir ein Bild geschickt: {file_name}"
f"{f' ({width}x{height}px)' if width else ''}" f"{f' ({width}x{height}px)' if width else ''}"
f", {size_kb}KB." f", {size_kb}KB."
f" Das Bild liegt unter: {file_path}") f" Das Bild liegt unter: {file_path}"
f" Warte auf Stefans Anweisung was du damit tun sollst.")
await self.send_to_core(text, source="app-file") await self.send_to_core(text, source="app-file")
# Dann App informieren (optional, darf nicht crashen) # Dann App informieren (optional, darf nicht crashen)
try: try:
@@ -1006,7 +1010,8 @@ class ARIABridge:
# ERST an aria-core senden # ERST an aria-core senden
text = (f"Stefan hat dir eine Datei geschickt: {file_name}" text = (f"Stefan hat dir eine Datei geschickt: {file_name}"
f" ({file_type}, {size_kb}KB)." f" ({file_type}, {size_kb}KB)."
f" Die Datei liegt unter: {file_path}") f" Die Datei liegt unter: {file_path}"
f" Warte auf Stefans Anweisung was du damit tun sollst.")
await self.send_to_core(text, source="app-file") await self.send_to_core(text, source="app-file")
try: try:
await self._send_to_rvs({ await self._send_to_rvs({
@@ -1216,8 +1221,10 @@ class ARIABridge:
logger.info("Keine Sprache erkannt — ignoriert") logger.info("Keine Sprache erkannt — ignoriert")
except sd.PortAudioError: except sd.PortAudioError:
logger.error("Audio-Geraet nicht verfuegbar — warte 5 Sekunden") if not hasattr(self, '_audio_warned'):
await asyncio.sleep(5) logger.warning("Audio-Geraet nicht verfuegbar — lokales Mikrofon deaktiviert (kein Spam mehr)")
self._audio_warned = True
await asyncio.sleep(60) # 60s statt 5s — spart Log-Spam
except Exception: except Exception:
logger.exception("Fehler in der Audio-Schleife") logger.exception("Fehler in der Audio-Schleife")
await asyncio.sleep(1) await asyncio.sleep(1)
+61
View File
@@ -201,6 +201,9 @@
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button> <button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
</div> </div>
<div class="chat-box" id="chat-box"></div> <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;">
<span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text">ARIA denkt...</span>
</div>
<div class="input-row"> <div class="input-row">
<input type="text" id="chat-input" placeholder="Nachricht an ARIA..."> <input type="text" id="chat-input" placeholder="Nachricht an ARIA...">
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button> <button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
@@ -216,6 +219,9 @@
<button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button> <button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button>
</div> </div>
<div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div> <div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div>
<div id="thinking-indicator-fs" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:6px;margin-top:4px;">
<span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text-fs">ARIA denkt...</span>
</div>
<div class="input-row" style="margin-top:8px;"> <div class="input-row" style="margin-top:8px;">
<input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}"> <input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}">
<button class="btn" onclick="testGatewayFS()">Gateway senden</button> <button class="btn" onclick="testGatewayFS()">Gateway senden</button>
@@ -507,6 +513,11 @@
if (msg.type === 'state') { updateState(msg.state); return; } if (msg.type === 'state') { updateState(msg.state); return; }
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; } if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
if (msg.type === 'agent_activity') {
updateThinkingIndicator(msg);
return;
}
if (msg.type === 'chat_final') { if (msg.type === 'chat_final') {
addChat('received', msg.text, 'chat:final'); addChat('received', msg.text, 'chat:final');
return; return;
@@ -883,6 +894,10 @@
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`; return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
}); });
const html = `${linked}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`; const html = `${linked}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
// In beide Chat-Boxen schreiben (normal + Vollbild) // In beide Chat-Boxen schreiben (normal + Vollbild)
for (const box of [chatBox, document.getElementById('chat-box-fs')]) { for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue; if (!box) continue;
@@ -930,6 +945,52 @@
if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen(); if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen();
}); });
// ── Thinking-Indikator ─────────────────────────────
let thinkingTimeout = null;
const TOOL_LABELS = {
'Bash': '\uD83D\uDDA5\uFE0F Shell-Befehl',
'WebFetch': '\uD83C\uDF10 Webseite abrufen',
'WebSearch': '\uD83D\uDD0D Suche',
'Read': '\uD83D\uDCC4 Datei lesen',
'Write': '\u270D\uFE0F Datei schreiben',
'Edit': '\u270D\uFE0F Datei bearbeiten',
'Grep': '\uD83D\uDD0D Code durchsuchen',
'Glob': '\uD83D\uDCC1 Dateien suchen',
'Agent': '\uD83E\uDD16 Sub-Agent',
};
function updateThinkingIndicator(msg) {
const indicators = [
document.getElementById('thinking-indicator'),
document.getElementById('thinking-indicator-fs'),
];
const texts = [
document.getElementById('thinking-text'),
document.getElementById('thinking-text-fs'),
];
if (msg.activity === 'idle') {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
if (thinkingTimeout) { clearTimeout(thinkingTimeout); thinkingTimeout = null; }
return;
}
let label = 'ARIA denkt...';
if (msg.activity === 'tool' && msg.tool) {
label = TOOL_LABELS[msg.tool] || `\uD83D\uDD27 ${msg.tool}`;
} else if (msg.activity === 'assistant') {
label = 'ARIA schreibt...';
}
indicators.forEach(el => { if (el) el.style.display = 'block'; });
texts.forEach(el => { if (el) el.textContent = label; });
// Auto-Hide nach 2min (falls idle Event verpasst wird — ARIA arbeitet max 15min)
if (thinkingTimeout) clearTimeout(thinkingTimeout);
thinkingTimeout = setTimeout(() => {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
}, 120000);
}
function openLightbox(mediaType, url) { function openLightbox(mediaType, url) {
const lb = document.getElementById('lightbox'); const lb = document.getElementById('lightbox');
if (mediaType === 'video') { if (mediaType === 'video') {
+115 -30
View File
@@ -74,8 +74,8 @@ function pipelineStart(method, text) {
pipelineStartTime = Date.now(); pipelineStartTime = Date.now();
if (pipelineTimeout) clearTimeout(pipelineTimeout); if (pipelineTimeout) clearTimeout(pipelineTimeout);
pipelineTimeout = setTimeout(() => { pipelineTimeout = setTimeout(() => {
if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 60s"); if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 10min");
}, 60000); }, 600000);
plog(`━━━ Pipeline Start: ${method} ━━━`); plog(`━━━ Pipeline Start: ${method} ━━━`);
plog(`Nachricht: "${text}"`); plog(`Nachricht: "${text}"`);
} }
@@ -319,10 +319,23 @@ function handleGatewayMessage(msg) {
if (event === "agent") { if (event === "agent") {
const data = payload.data || {}; const data = payload.data || {};
const delta = data.delta || ""; const delta = data.delta || "";
if (delta && payload.stream === "assistant") { const stream = payload.stream || "";
if (delta && stream === "assistant") {
broadcast({ type: "chat_delta", delta, payload }); broadcast({ type: "chat_delta", delta, payload });
} }
// agent Events nicht einzeln loggen (zu viele)
// Tool-Nutzung erkennen und broadcasten
if (stream === "tool_use" || data.type === "tool_use") {
const toolName = data.name || data.tool || payload.tool || "";
if (toolName) {
broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
log("info", "gateway", `Tool: ${toolName}`);
}
}
// Genereller Activity-Heartbeat (ARIA denkt)
broadcast({ type: "agent_activity", activity: stream || "thinking" });
return; return;
} }
@@ -338,6 +351,7 @@ function handleGatewayMessage(msg) {
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
broadcast({ type: "chat_final", text, payload }); broadcast({ type: "chat_final", text, payload });
broadcast({ type: "agent_activity", activity: "idle" });
return; return;
} }
@@ -413,14 +427,7 @@ function sendToGateway(text, isPipeline) {
log("info", "gateway", `chat.send [${reqId}]: "${text}"`); log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`); if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
// Nachricht auch an RVS senden damit die App sie sieht // Gateway-Nachrichten NICHT an RVS senden (sonst doppelter ARIA-Request via Bridge)
if (rvsWs && rvsWs.readyState === WebSocket.OPEN) {
rvsWs.send(JSON.stringify({
type: "chat",
payload: { text, sender: "diagnostic" },
timestamp: Date.now(),
}));
}
return true; return true;
} }
@@ -434,7 +441,13 @@ function connectRVS(forcePlain) {
return; return;
} }
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws (wenn erlaubt) // Alte Verbindung sauber schliessen
if (rvsWs) {
try { rvsWs.removeAllListeners(); rvsWs.close(); } catch (_) {}
rvsWs = null;
}
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws
const useTls = RVS_TLS === "true" && !forcePlain; const useTls = RVS_TLS === "true" && !forcePlain;
const proto = useTls ? "wss" : "ws"; const proto = useTls ? "wss" : "ws";
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`; const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
@@ -443,7 +456,18 @@ function connectRVS(forcePlain) {
broadcastState(); broadcastState();
log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`); log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`);
const ws = new WebSocket(url); let ws;
try {
ws = new WebSocket(url);
} catch (err) {
log("error", "rvs", `WebSocket erstellen fehlgeschlagen: ${err.message}`);
if (useTls && RVS_TLS_FALLBACK === "true") {
connectRVS(true);
}
return;
}
let fallbackTriggered = false;
ws.on("open", () => { ws.on("open", () => {
log("info", "rvs", `Verbunden (${proto})`); log("info", "rvs", `Verbunden (${proto})`);
@@ -451,6 +475,16 @@ function connectRVS(forcePlain) {
state.rvs.lastError = null; state.rvs.lastError = null;
rvsWs = ws; rvsWs = ws;
broadcastState(); broadcastState();
// Keepalive: alle 25s ein Ping senden damit die Verbindung nicht stirbt
const keepalive = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
try { ws.ping(); } catch (_) {}
} else {
clearInterval(keepalive);
}
}, 25000);
ws._keepalive = keepalive;
}); });
ws.on("message", (raw) => { ws.on("message", (raw) => {
@@ -458,11 +492,24 @@ function connectRVS(forcePlain) {
const msg = JSON.parse(raw.toString()); const msg = JSON.parse(raw.toString());
if (msg.type === "chat" && msg.payload) { if (msg.type === "chat" && msg.payload) {
const sender = msg.payload.sender || "?"; const sender = msg.payload.sender || "?";
// Eigene Nachrichten ignorieren (Echo)
if (sender === "diagnostic") return;
log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`); log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`);
if (pipelineActive && sender !== "diagnostic") { if (pipelineActive) {
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`); pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
} }
broadcast({ type: "rvs_chat", msg }); broadcast({ type: "rvs_chat", msg });
} else if (msg.type === "file_saved" && msg.payload) {
// Bild/Datei-Upload von der App — im Chat anzeigen
const name = msg.payload.name || "?";
const serverPath = msg.payload.serverPath || "";
const mimeType = msg.payload.mimeType || "";
log("info", "rvs", `Datei empfangen: ${name} (${serverPath})`);
// Als User-Nachricht mit Pfad broadcasten (Diagnostic zeigt Bilder inline)
broadcast({ type: "rvs_chat", msg: {
type: "chat",
payload: { text: `Anhang: ${name}\n${serverPath}`, sender: "user" }
}});
} else if (msg.type === "heartbeat") { } else if (msg.type === "heartbeat") {
// ignorieren // ignorieren
} else { } else {
@@ -473,10 +520,13 @@ function connectRVS(forcePlain) {
ws.on("close", () => { ws.on("close", () => {
log("warn", "rvs", "Verbindung geschlossen"); log("warn", "rvs", "Verbindung geschlossen");
if (ws._keepalive) clearInterval(ws._keepalive);
state.rvs.status = "disconnected"; state.rvs.status = "disconnected";
rvsWs = null; if (rvsWs === ws) rvsWs = null;
broadcastState(); broadcastState();
setTimeout(() => connectRVS(), 5000); if (!fallbackTriggered) {
setTimeout(() => connectRVS(), 5000);
}
}); });
ws.on("error", (err) => { ws.on("error", (err) => {
@@ -484,29 +534,64 @@ function connectRVS(forcePlain) {
state.rvs.lastError = err.message; state.rvs.lastError = err.message;
broadcastState(); broadcastState();
// TLS Fallback: wenn wss fehlschlaegt und Fallback erlaubt → ws versuchen // TLS Fallback
if (useTls && RVS_TLS_FALLBACK === "true") { if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
fallbackTriggered = true;
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://"); log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
ws.removeAllListeners(); try { ws.removeAllListeners(); ws.close(); } catch (_) {}
try { ws.close(); } catch (_) {} if (rvsWs === ws) rvsWs = null;
connectRVS(true); connectRVS(true);
} }
}); });
} }
function sendToRVS(text, isPipeline) { function sendToRVS(text, isPipeline) {
if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) { if (!RVS_HOST || !RVS_TOKEN) {
log("error", "rvs", "Nicht verbunden"); log("error", "rvs", "Nicht konfiguriert");
if (isPipeline) pipelineEnd(false, "RVS nicht verbunden"); if (isPipeline) pipelineEnd(false, "RVS nicht konfiguriert");
return false; return false;
} }
rvsWs.send(JSON.stringify({ // Frische WebSocket-Verbindung fuer jede Nachricht (Zombie-Schutz)
const proto = RVS_TLS === "true" ? "wss" : "ws";
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
const msg = JSON.stringify({
type: "chat", type: "chat",
payload: { text, sender: "diagnostic" }, payload: { text, sender: "diagnostic" },
timestamp: Date.now(), timestamp: Date.now(),
})); });
log("info", "rvs", `Gesendet via RVS: "${text}"`);
log("info", "rvs", `Sende via frische Verbindung: ${url.split('?')[0]}`);
const freshWs = new WebSocket(url);
freshWs.on("open", () => {
freshWs.send(msg);
log("info", "rvs", `Gesendet via RVS: "${text}"`);
// Verbindung offen lassen fuer Antwort-Empfang, nach 5min schliessen
setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 300000);
});
freshWs.on("message", (raw) => {
try {
const resp = JSON.parse(raw.toString());
if (resp.type === "chat" && resp.payload) {
const sender = resp.payload.sender || "?";
// Eigene Nachrichten und STT ignorieren (werden von persistenter Verbindung gehandelt)
if (sender === "diagnostic" || sender === "stt") return;
log("info", "rvs", `Chat von ${sender}: "${(resp.payload.text || "").slice(0, 100)}"`);
if (pipelineActive && sender !== "diagnostic") {
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(resp.payload.text || "").slice(0, 120)}"`);
}
broadcast({ type: "rvs_chat", msg: resp });
} else if (resp.type !== "heartbeat") {
log("debug", "rvs", `Nachricht: ${JSON.stringify(resp).slice(0, 150)}`);
}
} catch {}
});
freshWs.on("error", (err) => {
log("error", "rvs", `Sende-Fehler: ${err.message}`);
if (isPipeline) pipelineEnd(false, `RVS Fehler: ${err.message}`);
});
if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`); if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`);
return true; return true;
} }
@@ -526,7 +611,7 @@ async function testProxy(prompt) {
const modelsRes = await fetch(healthUrl, { const modelsRes = await fetch(healthUrl, {
headers: { "Authorization": "Bearer not-needed" }, headers: { "Authorization": "Bearer not-needed" },
signal: AbortSignal.timeout(10000), signal: AbortSignal.timeout(30000),
}); });
if (!modelsRes.ok) { if (!modelsRes.ok) {
@@ -553,7 +638,7 @@ async function testProxy(prompt) {
} }
// Schritt 2: Chat Completion testen (kurzer Prompt) // Schritt 2: Chat Completion testen (kurzer Prompt)
const testPrompt = prompt || "Antworte mit genau einem Wort: Ping"; const testPrompt = prompt || "Antworte in einem Satz: Wer bist du und funktionierst du?";
log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`); log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`);
const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, { const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, {
@@ -567,7 +652,7 @@ async function testProxy(prompt) {
messages: [{ role: "user", content: testPrompt }], messages: [{ role: "user", content: testPrompt }],
max_tokens: 200, max_tokens: 200,
}), }),
signal: AbortSignal.timeout(30000), signal: AbortSignal.timeout(120000), // 2min — Cold Start braucht Zeit
}); });
if (!chatRes.ok) { if (!chatRes.ok) {
+1
View File
@@ -19,6 +19,7 @@ services:
volumes: volumes:
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json) - ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
- ./aria-data/ssh:/root/.ssh:ro # SSH Keys fuer VM-Zugriff (aria-wohnung) - ./aria-data/ssh:/root/.ssh:ro # SSH Keys fuer VM-Zugriff (aria-wohnung)
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
environment: environment:
- HOST=0.0.0.0 - HOST=0.0.0.0
- SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash) - SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash)
+19
View File
@@ -1,6 +1,25 @@
bildupload ghet noch nicht. bildupload ghet noch nicht.
#erledigt
sprachnachrichten werden nicht als zweite nachricht dargestellt, damit man weiß was man gesendet hat sprachnachrichten werden nicht als zweite nachricht dargestellt, damit man weiß was man gesendet hat
# ende
cache leeren, bilder werden nicht neu geladen beim antippen. cache leeren, bilder werden nicht neu geladen beim antippen.
autoload geht nicht autoload geht nicht
wenn man auf das ohr zum hören klickt stürzt ab wenn man auf das ohr zum hören klickt stürzt ab
aria liest die nachrichten nicht vor aria liest die nachrichten nicht vor
# erledigt autoscroll geht doch noch nicht zur letzten nachricht
unserer memory brain
# ende
bilder im chat größer darstellen
# ende
die viper voices downloaden über die diagnostic
# ende
+9 -7
View File
@@ -58,17 +58,19 @@ echo -e "${GREEN}[1/5] Versionsnummern auf $VERSION setzen...${NC}"
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" android/package.json sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" android/package.json
echo -e " ${GREEN}${NC} package.json → $VERSION" echo -e " ${GREEN}${NC} package.json → $VERSION"
# build.gradle: versionName + versionCode (aus Major.Minor.Patch berechnen) # build.gradle: versionName + versionCode (aus Version berechnen)
MAJOR=$(echo "$VERSION" | cut -d. -f1) # Unterstuetzt 3-stellig (1.2.3) und 4-stellig (0.0.1.7)
MINOR=$(echo "$VERSION" | cut -d. -f2) IFS='.' read -ra VER_PARTS <<< "$VERSION"
PATCH=$(echo "$VERSION" | cut -d. -f3) V1=${VER_PARTS[0]:-0}; V2=${VER_PARTS[1]:-0}; V3=${VER_PARTS[2]:-0}; V4=${VER_PARTS[3]:-0}
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH)) VERSION_CODE=$((V1 * 1000000 + V2 * 10000 + V3 * 100 + V4))
# Mindestens 1 (Android erfordert versionCode >= 1)
[ "$VERSION_CODE" -lt 1 ] && VERSION_CODE=1
sed -i "s/versionName \"[^\"]*\"/versionName \"$VERSION\"/" android/android/app/build.gradle sed -i "s/versionName \"[^\"]*\"/versionName \"$VERSION\"/" android/android/app/build.gradle
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/android/app/build.gradle sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/android/app/build.gradle
echo -e " ${GREEN}${NC} build.gradle → versionName $VERSION, versionCode $VERSION_CODE" echo -e " ${GREEN}${NC} build.gradle → versionName $VERSION, versionCode $VERSION_CODE"
# SettingsScreen: Anzeige-Version # SettingsScreen: Anzeige-Version (beliebiges Versionsformat)
sed -i "s/Version [0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]* [^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx sed -i "s/Version [0-9][0-9.]*[^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx
echo -e " ${GREEN}${NC} SettingsScreen → Version $VERSION" echo -e " ${GREEN}${NC} SettingsScreen → Version $VERSION"
echo "" echo ""