Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fac87474ec | |||
| 8227266aea | |||
| 5d24e01d4b | |||
| 4fe72cc4a8 | |||
| eeeb1d43f5 | |||
| 0044e222db | |||
| 048d231b60 |
@@ -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 10506
|
versionCode 10509
|
||||||
versionName "0.1.5.6"
|
versionName "0.1.5.9"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
import com.facebook.react.bridge.ReactMethod
|
import com.facebook.react.bridge.ReactMethod
|
||||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lauscht auf Anruf-Statusaenderungen — wenn das Telefon klingelt oder ein
|
* Lauscht auf Anruf-Statusaenderungen — wenn das Telefon klingelt oder ein
|
||||||
@@ -35,6 +36,11 @@ class PhoneCallModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
|
|||||||
private var legacyListener: PhoneStateListener? = null
|
private var legacyListener: PhoneStateListener? = null
|
||||||
private var modernCallback: Any? = null // TelephonyCallback ab API 31
|
private var modernCallback: Any? = null // TelephonyCallback ab API 31
|
||||||
private var lastState: Int = TelephonyManager.CALL_STATE_IDLE
|
private var lastState: Int = TelephonyManager.CALL_STATE_IDLE
|
||||||
|
// Eigener Single-Thread-Executor statt mainExecutor — der wird bei
|
||||||
|
// pausierter Activity verzoegert oder gar nicht abgearbeitet, der eigene
|
||||||
|
// Thread laeuft unabhaengig solange der App-Prozess lebt (was er ja tut,
|
||||||
|
// wir haben einen Foreground-Service der das garantiert).
|
||||||
|
private val callbackExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun start(promise: Promise) {
|
fun start(promise: Promise) {
|
||||||
@@ -59,7 +65,7 @@ class PhoneCallModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
|
|||||||
handleStateChange(state)
|
handleStateChange(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tm.registerTelephonyCallback(reactApplicationContext.mainExecutor, cb)
|
tm.registerTelephonyCallback(callbackExecutor, cb)
|
||||||
modernCallback = cb
|
modernCallback = cb
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.5.6",
|
"version": "0.1.5.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -149,6 +149,22 @@ const MAX_THOUGHTS = 500;
|
|||||||
// im Gespraechsmodus bei sehr vielen Nachrichten.
|
// im Gespraechsmodus bei sehr vielen Nachrichten.
|
||||||
const capMessages = (msgs: ChatMessage[]): ChatMessage[] =>
|
const capMessages = (msgs: ChatMessage[]): ChatMessage[] =>
|
||||||
msgs.length > MAX_MEMORY_MESSAGES ? msgs.slice(-MAX_MEMORY_MESSAGES) : msgs;
|
msgs.length > MAX_MEMORY_MESSAGES ? msgs.slice(-MAX_MEMORY_MESSAGES) : msgs;
|
||||||
|
|
||||||
|
// Bridge fuegt User-Texten Praefixe in eckigen Klammern hinzu damit Brain
|
||||||
|
// Kontext hat (GPS-Position, Barge-In-Hint etc.). Diese sollen nicht in der
|
||||||
|
// Bubble auftauchen — nur Brain sieht sie. Filtert alle aufeinanderfolgenden
|
||||||
|
// [...]-Bloecke am Textanfang weg, inkl. der Trennleerzeichen dahinter.
|
||||||
|
function stripSystemHints(text: string): string {
|
||||||
|
if (!text) return text;
|
||||||
|
let out = text;
|
||||||
|
// Mehrere Hints koennen aneinanderhaengen — "[A] [B] Hallo" → "Hallo"
|
||||||
|
while (true) {
|
||||||
|
const m = out.match(/^\s*\[[^\]]*\]\s*/);
|
||||||
|
if (!m) break;
|
||||||
|
out = out.slice(m[0].length);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
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';
|
||||||
|
|
||||||
@@ -279,6 +295,12 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Gerätelokale TTS-Config: globaler Toggle (aus Settings) + temporäres Muten (Mund-Button)
|
// Gerätelokale TTS-Config: globaler Toggle (aus Settings) + temporäres Muten (Mund-Button)
|
||||||
const [ttsDeviceEnabled, setTtsDeviceEnabled] = useState(true);
|
const [ttsDeviceEnabled, setTtsDeviceEnabled] = useState(true);
|
||||||
const [ttsMuted, setTtsMuted] = useState(false);
|
const [ttsMuted, setTtsMuted] = useState(false);
|
||||||
|
// System-Hints in Bubble: Bridge fuegt User-Text Praefixe wie
|
||||||
|
// "[Stefans aktuelle GPS-Position: ...]" oder "[Hinweis: Stefan hat
|
||||||
|
// dich gerade unterbrochen...]" hinzu damit Brain Kontext hat. Die
|
||||||
|
// App soll sie standardmaessig NICHT anzeigen — Stefan sieht sonst
|
||||||
|
// jeden Hint mit. Toggle in Settings.
|
||||||
|
const [showSystemHints, setShowSystemHints] = useState(false);
|
||||||
// Gerätelokale XTTS-Voice-Wahl (bevorzugt gegenueber dem globalen Default)
|
// Gerätelokale XTTS-Voice-Wahl (bevorzugt gegenueber dem globalen Default)
|
||||||
const localXttsVoiceRef = useRef<string>('');
|
const localXttsVoiceRef = useRef<string>('');
|
||||||
// Geraetelokale TTS-Wiedergabegeschwindigkeit (speed-Param an F5-TTS)
|
// Geraetelokale TTS-Wiedergabegeschwindigkeit (speed-Param an F5-TTS)
|
||||||
@@ -446,6 +468,8 @@ const ChatScreen: React.FC = () => {
|
|||||||
ttsSpeedRef.current = await loadTtsSpeed();
|
ttsSpeedRef.current = await loadTtsSpeed();
|
||||||
const gps = await AsyncStorage.getItem('aria_gps_enabled');
|
const gps = await AsyncStorage.getItem('aria_gps_enabled');
|
||||||
setGpsEnabled(gps === 'true');
|
setGpsEnabled(gps === 'true');
|
||||||
|
const hints = await AsyncStorage.getItem('aria_show_hints');
|
||||||
|
setShowSystemHints(hints === 'true'); // default false
|
||||||
};
|
};
|
||||||
loadSettings();
|
loadSettings();
|
||||||
const interval = setInterval(loadSettings, 2000);
|
const interval = setInterval(loadSettings, 2000);
|
||||||
@@ -480,14 +504,40 @@ const ChatScreen: React.FC = () => {
|
|||||||
return () => { phoneCallService.stop().catch(() => {}); };
|
return () => { phoneCallService.stop().catch(() => {}); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// App-Resume: kurzer Wake-Word-Cooldown — beim Wechsel Background→Foreground
|
// App-Resume: drei Schutzmaßnahmen gegen verirrte Wake-Word-Trigger
|
||||||
// gibt's haeufig Audio-Pegel-Spikes (AudioFocus-Switch, AudioTrack re-route)
|
// beim Wechsel Background→Foreground:
|
||||||
// die openWakeWord sonst faelschlich als Wake-Word interpretiert.
|
// (a) Cooldown 3s — Audio-Pegel-Spikes (AudioFocus-Switch, AudioTrack
|
||||||
|
// re-route) sollen openWakeWord nicht faelschlich triggern
|
||||||
|
// (b) Wenn die App laenger im Hintergrund war und in 'conversing'
|
||||||
|
// zurueckkommt: vermutlich false-positive durch ein Hintergrund-
|
||||||
|
// Geraeusch (TV, Husten etc.) waehrend Stefan gar nicht da war.
|
||||||
|
// Wir verwerfen den Trigger und gehen zurueck zu 'armed'.
|
||||||
|
// (c) Aktuelle Aufnahme abbrechen falls sie aus dem false-positive
|
||||||
|
// gerade gestartet wurde.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastState: string = AppState.currentState;
|
let lastState: string = AppState.currentState;
|
||||||
|
let lastBackgroundAt = 0;
|
||||||
const sub = AppState.addEventListener('change', (next) => {
|
const sub = AppState.addEventListener('change', (next) => {
|
||||||
if (lastState !== 'active' && next === 'active') {
|
if (next === 'background' || next === 'inactive') {
|
||||||
wakeWordService.setResumeCooldown(1500);
|
lastBackgroundAt = Date.now();
|
||||||
|
} else if (lastState !== 'active' && next === 'active') {
|
||||||
|
wakeWordService.setResumeCooldown(3000);
|
||||||
|
const bgDur = lastBackgroundAt > 0 ? Date.now() - lastBackgroundAt : 0;
|
||||||
|
// Bei laengerer Hintergrund-Zeit (>30s): pruefen ob ein frisches
|
||||||
|
// Wake-Word getriggert wurde wahrend die App weg war — wenn ja,
|
||||||
|
// verwerfen + laufende Aufnahme stoppen.
|
||||||
|
if (bgDur > 30_000) {
|
||||||
|
wakeWordService.discardIfFreshlyTriggered(15_000).then(discarded => {
|
||||||
|
if (discarded) {
|
||||||
|
try { audioService.cancelRecording(); } catch {}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
// PhoneCall-Listener pruefen: kann passieren dass der nach laengerer
|
||||||
|
// Hintergrund-Zeit verloren geht (Bridge-Context recreated). Refresh
|
||||||
|
// versucht ihn neu zu attachen falls noetig — sonst kriegt die App
|
||||||
|
// bei display-aus / minimized keine Anruf-Events mit.
|
||||||
|
phoneCallService.refresh().catch(() => {});
|
||||||
}
|
}
|
||||||
lastState = next;
|
lastState = next;
|
||||||
});
|
});
|
||||||
@@ -2006,7 +2056,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
{/* Text (nicht anzeigen wenn nur "Anhang empfangen" und ein Bild da ist) */}
|
{/* Text (nicht anzeigen wenn nur "Anhang empfangen" und ein Bild da ist) */}
|
||||||
{!(item.text === 'Anhang empfangen' && item.attachments?.some(a => a.type === 'image' && a.uri)) && (
|
{!(item.text === 'Anhang empfangen' && item.attachments?.some(a => a.type === 'image' && a.uri)) && (
|
||||||
<MessageText
|
<MessageText
|
||||||
text={item.text}
|
text={showSystemHints ? item.text : stripSystemHints(item.text)}
|
||||||
style={[styles.messageText, isUser ? styles.userText : styles.ariaText]}
|
style={[styles.messageText, isUser ? styles.userText : styles.ariaText]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [gpsEnabled, setGpsEnabled] = useState(false);
|
const [gpsEnabled, setGpsEnabled] = useState(false);
|
||||||
const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive());
|
const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive());
|
||||||
const [backgroundMode, setBackgroundMode] = useState(true); // Default an
|
const [backgroundMode, setBackgroundMode] = useState(true); // Default an
|
||||||
|
const [showSystemHints, setShowSystemHints] = useState(false); // Default aus
|
||||||
const [scannerVisible, setScannerVisible] = useState(false);
|
const [scannerVisible, setScannerVisible] = useState(false);
|
||||||
const [logTab, setLogTab] = useState<LogTab>('live');
|
const [logTab, setLogTab] = useState<LogTab>('live');
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
@@ -202,6 +203,10 @@ const SettingsScreen: React.FC = () => {
|
|||||||
// Default ist an — nur explicit 'false' deaktiviert
|
// Default ist an — nur explicit 'false' deaktiviert
|
||||||
setBackgroundMode(saved !== 'false');
|
setBackgroundMode(saved !== 'false');
|
||||||
});
|
});
|
||||||
|
AsyncStorage.getItem('aria_show_hints').then(saved => {
|
||||||
|
// Default ist aus — nur explicit 'true' aktiviert
|
||||||
|
setShowSystemHints(saved === 'true');
|
||||||
|
});
|
||||||
// gpsTrackingService status syncen + auf Aenderungen lauschen
|
// gpsTrackingService status syncen + auf Aenderungen lauschen
|
||||||
setGpsTracking(gpsTrackingService.isActive());
|
setGpsTracking(gpsTrackingService.isActive());
|
||||||
const offGps = gpsTrackingService.onChange(setGpsTracking);
|
const offGps = gpsTrackingService.onChange(setGpsTracking);
|
||||||
@@ -616,6 +621,13 @@ const SettingsScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// --- System-Hints Toggle ---
|
||||||
|
|
||||||
|
const handleShowSystemHintsToggle = useCallback((value: boolean) => {
|
||||||
|
setShowSystemHints(value);
|
||||||
|
AsyncStorage.setItem('aria_show_hints', String(value)).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// --- XTTS Voice ---
|
// --- XTTS Voice ---
|
||||||
|
|
||||||
const selectVoice = useCallback((voiceName: string) => {
|
const selectVoice = useCallback((voiceName: string) => {
|
||||||
@@ -1103,6 +1115,28 @@ const SettingsScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* === Bubble-Anzeige === */}
|
||||||
|
<Text style={styles.sectionTitle}>Chat-Bubbles</Text>
|
||||||
|
<View style={styles.card}>
|
||||||
|
<View style={styles.toggleRow}>
|
||||||
|
<View style={styles.toggleInfo}>
|
||||||
|
<Text style={styles.toggleLabel}>System-Hints in Bubbles anzeigen</Text>
|
||||||
|
<Text style={styles.toggleHint}>
|
||||||
|
Wenn aktiviert: GPS-Position, Barge-In-Hinweise und andere
|
||||||
|
System-Praefixe in eckigen Klammern bleiben in der User-Bubble
|
||||||
|
sichtbar (Debug). Standardmaessig versteckt — Brain bekommt sie
|
||||||
|
trotzdem, sie sind nur fuer dich nicht relevant.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={showSystemHints}
|
||||||
|
onValueChange={handleShowSystemHintsToggle}
|
||||||
|
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
|
||||||
|
thumbColor={showSystemHints ? '#FFFFFF' : '#666680'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* === Hintergrund-Modus === */}
|
{/* === Hintergrund-Modus === */}
|
||||||
<Text style={styles.sectionTitle}>Hintergrund-Modus</Text>
|
<Text style={styles.sectionTitle}>Hintergrund-Modus</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
|
|||||||
@@ -727,6 +727,31 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Aufnahme abbrechen ohne RecordingResult zu emittieren — z.B. bei
|
||||||
|
* Wake-Word-False-Positive beim App-Resume aus laengerem Hintergrund.
|
||||||
|
* Aufgenommene Datei wird sofort verworfen. */
|
||||||
|
async cancelRecording(): Promise<void> {
|
||||||
|
if (this.recordingState !== 'recording') return;
|
||||||
|
console.log('[Audio] Aufnahme abgebrochen (cancel)');
|
||||||
|
this.vadEnabled = false;
|
||||||
|
if (this.vadTimer) { clearInterval(this.vadTimer); this.vadTimer = null; }
|
||||||
|
if (this.maxDurationTimer) { clearTimeout(this.maxDurationTimer); this.maxDurationTimer = null; }
|
||||||
|
if (this.noSpeechTimer) { clearTimeout(this.noSpeechTimer); this.noSpeechTimer = null; }
|
||||||
|
try {
|
||||||
|
const path = await this.recorder.stopRecorder();
|
||||||
|
this.recorder.removeRecordBackListener();
|
||||||
|
// Datei loeschen wenn da
|
||||||
|
if (path && path !== 'Already stopped') {
|
||||||
|
const local = path.replace(/^file:\/\//, '');
|
||||||
|
try { await RNFS.unlink(local); } catch {}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Audio] cancelRecording stop fehlgeschlagen:', err);
|
||||||
|
}
|
||||||
|
this._releaseFocusDeferred();
|
||||||
|
this.setState('idle');
|
||||||
|
}
|
||||||
|
|
||||||
/** Aufnahme stoppen und Ergebnis zurueckgeben */
|
/** Aufnahme stoppen und Ergebnis zurueckgeben */
|
||||||
async stopRecording(): Promise<RecordingResult | null> {
|
async stopRecording(): Promise<RecordingResult | null> {
|
||||||
if (this.recordingState !== 'recording') {
|
if (this.recordingState !== 'recording') {
|
||||||
|
|||||||
@@ -43,6 +43,42 @@ class PhoneCallService {
|
|||||||
/** Damit Resume nach VoIP-Loss nicht doppelt feuert wenn auch
|
/** Damit Resume nach VoIP-Loss nicht doppelt feuert wenn auch
|
||||||
* TelephonyManager-IDLE-Event kommt. */
|
* TelephonyManager-IDLE-Event kommt. */
|
||||||
private interruptedByFocus: boolean = false;
|
private interruptedByFocus: boolean = false;
|
||||||
|
/** True wenn der TelephonyManager-Listener (Pfad 1) wirklich registriert
|
||||||
|
* ist. False wenn READ_PHONE_STATE abgelehnt wurde oder Native nicht ging. */
|
||||||
|
private telephonyAttached: boolean = false;
|
||||||
|
|
||||||
|
/** Status fuer Diagnose: laeuft die Anruf-Erkennung tatsaechlich? */
|
||||||
|
status(): { focusAttached: boolean; telephonyAttached: boolean } {
|
||||||
|
return {
|
||||||
|
focusAttached: this.focusSubscription !== null,
|
||||||
|
telephonyAttached: this.telephonyAttached,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nach App-Resume: pruefen ob die Listener noch leben. Wenn der
|
||||||
|
* TelephonyManager-Listener verloren ging (kann passieren wenn der
|
||||||
|
* React-Bridge-Context recreated wurde), neu attachen. */
|
||||||
|
async refresh(): Promise<void> {
|
||||||
|
if (!this.started) return;
|
||||||
|
if (this.telephonyAttached) return; // alles ok
|
||||||
|
if (!PhoneCall) return;
|
||||||
|
try {
|
||||||
|
const ok = await PhoneCall.start();
|
||||||
|
if (ok) {
|
||||||
|
if (!this.subscription) {
|
||||||
|
const emitter = new NativeEventEmitter(NativeModules.PhoneCall as any);
|
||||||
|
this.subscription = emitter.addListener(
|
||||||
|
'PhoneCallStateChanged',
|
||||||
|
(e: { state: PhoneState }) => this._onStateChanged(e.state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.telephonyAttached = true;
|
||||||
|
console.log('[PhoneCall] refresh: TelephonyManager-Listener re-attached');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.warn('[PhoneCall] refresh fehlgeschlagen:', err?.message || err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async start(): Promise<boolean> {
|
async start(): Promise<boolean> {
|
||||||
if (this.started || Platform.OS !== 'android') return false;
|
if (this.started || Platform.OS !== 'android') return false;
|
||||||
@@ -82,7 +118,10 @@ class PhoneCallService {
|
|||||||
'PhoneCallStateChanged',
|
'PhoneCallStateChanged',
|
||||||
(e: { state: PhoneState }) => this._onStateChanged(e.state),
|
(e: { state: PhoneState }) => this._onStateChanged(e.state),
|
||||||
);
|
);
|
||||||
|
this.telephonyAttached = true;
|
||||||
console.log('[PhoneCall] TelephonyManager-Listener aktiv');
|
console.log('[PhoneCall] TelephonyManager-Listener aktiv');
|
||||||
|
} else {
|
||||||
|
console.warn('[PhoneCall] PhoneCall.start() lieferte false — Native-Listener nicht aktiv');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('[PhoneCall] READ_PHONE_STATE abgelehnt — VoIP-Calls werden trotzdem ueber AudioFocus erkannt');
|
console.warn('[PhoneCall] READ_PHONE_STATE abgelehnt — VoIP-Calls werden trotzdem ueber AudioFocus erkannt');
|
||||||
@@ -108,6 +147,7 @@ class PhoneCallService {
|
|||||||
this.started = false;
|
this.started = false;
|
||||||
this.lastState = 'idle';
|
this.lastState = 'idle';
|
||||||
this.interruptedByFocus = false;
|
this.interruptedByFocus = false;
|
||||||
|
this.telephonyAttached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onStateChanged(state: PhoneState): void {
|
private _onStateChanged(state: PhoneState): void {
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ class WakeWordService {
|
|||||||
* oft einen Audio-Pegel-Spike (AudioFocus-Switch, AudioTrack re-route),
|
* oft einen Audio-Pegel-Spike (AudioFocus-Switch, AudioTrack re-route),
|
||||||
* der openWakeWord faelschlich triggern kann. */
|
* der openWakeWord faelschlich triggern kann. */
|
||||||
private cooldownUntilMs: number = 0;
|
private cooldownUntilMs: number = 0;
|
||||||
|
/** Zeitpunkt des letzten echten Wake-Word-Triggers — gebraucht damit
|
||||||
|
* ChatScreen entscheiden kann ob ein 'conversing'-State bei App-Resume
|
||||||
|
* ein false-positive war (Wake-Word im Hintergrund getriggert waehrend
|
||||||
|
* Stefan gar nicht in der App war). */
|
||||||
|
private lastTriggerAt: number = 0;
|
||||||
|
|
||||||
private keyword: WakeKeyword = DEFAULT_KEYWORD;
|
private keyword: WakeKeyword = DEFAULT_KEYWORD;
|
||||||
private nativeReady: boolean = false;
|
private nativeReady: boolean = false;
|
||||||
@@ -231,6 +236,7 @@ class WakeWordService {
|
|||||||
}
|
}
|
||||||
console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)',
|
console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)',
|
||||||
this.keyword, this.state, this.bargeListening);
|
this.keyword, this.state, this.bargeListening);
|
||||||
|
this.lastTriggerAt = now;
|
||||||
if (this.nativeReady && OpenWakeWord) {
|
if (this.nativeReady && OpenWakeWord) {
|
||||||
try { await OpenWakeWord.stop(); } catch {}
|
try { await OpenWakeWord.stop(); } catch {}
|
||||||
}
|
}
|
||||||
@@ -341,6 +347,33 @@ class WakeWordService {
|
|||||||
this.setState('off');
|
this.setState('off');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wenn ein conversing-State auf einem Wake-Word-Trigger juenger als
|
||||||
|
* maxAgeMs basiert: false-positive verwerfen, zurueck zu armed.
|
||||||
|
* Wird vom ChatScreen aufgerufen wenn die App aus laengerem Hintergrund
|
||||||
|
* zurueck kommt — dann ist ein „gerade getriggertes" Wake-Word sehr
|
||||||
|
* wahrscheinlich ein TV-Spike, Husten, ARIAs eigene TTS-Aufnahme etc.
|
||||||
|
* Returnt true wenn verworfen wurde. */
|
||||||
|
async discardIfFreshlyTriggered(maxAgeMs: number = 10_000): Promise<boolean> {
|
||||||
|
if (this.state !== 'conversing') return false;
|
||||||
|
if (this.lastTriggerAt === 0) return false;
|
||||||
|
const age = Date.now() - this.lastTriggerAt;
|
||||||
|
if (age > maxAgeMs) return false;
|
||||||
|
console.log('[WakeWord] Resume: verwerfe verdaechtiges conversing (age=%dms)', age);
|
||||||
|
this.lastTriggerAt = 0;
|
||||||
|
if (this.nativeReady && OpenWakeWord) {
|
||||||
|
try {
|
||||||
|
await OpenWakeWord.start();
|
||||||
|
ToastAndroid.show('Hintergrund-Trigger verworfen — lausche wieder', ToastAndroid.SHORT);
|
||||||
|
this.setState('armed');
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[WakeWord] re-arm nach discard fehlgeschlagen:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState('off');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten */
|
/** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten */
|
||||||
async resume(): Promise<void> {
|
async resume(): Promise<void> {
|
||||||
if (this.state !== 'conversing') return;
|
if (this.state !== 'conversing') return;
|
||||||
|
|||||||
+8
-32
@@ -320,8 +320,7 @@
|
|||||||
<input type="file" id="diag-file-input" multiple accept="image/*,application/pdf,.doc,.docx,.txt" style="display:none;" onchange="handleDiagFileSelect(this.files)">
|
<input type="file" id="diag-file-input" multiple accept="image/*,application/pdf,.doc,.docx,.txt" style="display:none;" onchange="handleDiagFileSelect(this.files)">
|
||||||
</label>
|
</label>
|
||||||
<textarea id="chat-input" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" onpaste="handleDiagPaste(event)" oninput="autoResizeTextarea(this)"></textarea>
|
<textarea id="chat-input" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" onpaste="handleDiagPaste(event)" oninput="autoResizeTextarea(this)"></textarea>
|
||||||
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
|
<button class="btn" id="btn-rvs" onclick="testRVS()">Senden</button>
|
||||||
<button class="btn" id="btn-rvs" onclick="testRVS()">Via RVS senden</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,8 +337,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="input-row" style="margin-top:8px;">
|
<div class="input-row" style="margin-top:8px;">
|
||||||
<textarea id="chat-input-fs" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" oninput="autoResizeTextarea(this)"></textarea>
|
<textarea id="chat-input-fs" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" oninput="autoResizeTextarea(this)"></textarea>
|
||||||
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
|
<button class="btn" onclick="testRVSFS()">Senden</button>
|
||||||
<button class="btn" onclick="testRVSFS()">Via RVS senden</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -367,7 +365,6 @@
|
|||||||
<div style="padding: 0 12px;">
|
<div style="padding: 0 12px;">
|
||||||
<div class="tab-bar">
|
<div class="tab-bar">
|
||||||
<button class="tab-btn active" data-tab="all" onclick="switchTab('all')">Alle <span class="tab-count" id="count-all">0</span></button>
|
<button class="tab-btn active" data-tab="all" onclick="switchTab('all')">Alle <span class="tab-count" id="count-all">0</span></button>
|
||||||
<button class="tab-btn" data-tab="gateway" onclick="switchTab('gateway')">Gateway <span class="tab-count" id="count-gateway">0</span></button>
|
|
||||||
<button class="tab-btn" data-tab="rvs" onclick="switchTab('rvs')">RVS <span class="tab-count" id="count-rvs">0</span></button>
|
<button class="tab-btn" data-tab="rvs" onclick="switchTab('rvs')">RVS <span class="tab-count" id="count-rvs">0</span></button>
|
||||||
<button class="tab-btn" data-tab="proxy" onclick="switchTab('proxy')">Proxy <span class="tab-count" id="count-proxy">0</span></button>
|
<button class="tab-btn" data-tab="proxy" onclick="switchTab('proxy')">Proxy <span class="tab-count" id="count-proxy">0</span></button>
|
||||||
<button class="tab-btn" data-tab="bridge" onclick="switchTab('bridge')">Bridge <span class="tab-count" id="count-bridge">0</span></button>
|
<button class="tab-btn" data-tab="bridge" onclick="switchTab('bridge')">Bridge <span class="tab-count" id="count-bridge">0</span></button>
|
||||||
@@ -386,7 +383,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-box" id="log-all"></div>
|
<div class="log-box" id="log-all"></div>
|
||||||
<div class="log-box hidden" id="log-gateway"></div>
|
|
||||||
<div class="log-box hidden" id="log-rvs"></div>
|
<div class="log-box hidden" id="log-rvs"></div>
|
||||||
<div class="log-box hidden" id="log-proxy"></div>
|
<div class="log-box hidden" id="log-proxy"></div>
|
||||||
<div class="log-box hidden" id="log-bridge"></div>
|
<div class="log-box hidden" id="log-bridge"></div>
|
||||||
@@ -1118,13 +1114,12 @@
|
|||||||
const btnScroll = document.getElementById('btn-scroll');
|
const btnScroll = document.getElementById('btn-scroll');
|
||||||
let ws;
|
let ws;
|
||||||
let activeTab = 'all';
|
let activeTab = 'all';
|
||||||
const DOCKER_TABS = ['gateway', 'proxy', 'bridge'];
|
const DOCKER_TABS = ['proxy', 'bridge'];
|
||||||
const autoScroll = { all: true, gateway: true, rvs: true, proxy: true, bridge: true, server: true, trace: true };
|
const autoScroll = { all: true, rvs: true, proxy: true, bridge: true, server: true, trace: true };
|
||||||
const logCounts = { all: 0, gateway: 0, rvs: 0, proxy: 0, bridge: 0, server: 0, trace: 0 };
|
const logCounts = { all: 0, rvs: 0, proxy: 0, bridge: 0, server: 0, trace: 0 };
|
||||||
|
|
||||||
const logBoxes = {
|
const logBoxes = {
|
||||||
all: document.getElementById('log-all'),
|
all: document.getElementById('log-all'),
|
||||||
gateway: document.getElementById('log-gateway'),
|
|
||||||
rvs: document.getElementById('log-rvs'),
|
rvs: document.getElementById('log-rvs'),
|
||||||
proxy: document.getElementById('log-proxy'),
|
proxy: document.getElementById('log-proxy'),
|
||||||
bridge: document.getElementById('log-bridge'),
|
bridge: document.getElementById('log-bridge'),
|
||||||
@@ -1178,7 +1173,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapSourceToTab(source) {
|
function mapSourceToTab(source) {
|
||||||
if (source === 'gateway') return 'gateway';
|
// Gateway-Source: deprecated — falls noch was reinkommt zeigen wir's
|
||||||
|
// einfach unter 'server'. Spart einen toten Tab.
|
||||||
|
if (source === 'gateway') return 'server';
|
||||||
if (source === 'rvs') return 'rvs';
|
if (source === 'rvs') return 'rvs';
|
||||||
if (source === 'proxy') return 'proxy';
|
if (source === 'proxy') return 'proxy';
|
||||||
if (source === 'bridge') return 'bridge';
|
if (source === 'bridge') return 'bridge';
|
||||||
@@ -1620,18 +1617,6 @@
|
|||||||
renderDiagPending();
|
renderDiagPending();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testGateway() {
|
|
||||||
const input = document.getElementById('chat-input');
|
|
||||||
const text = input.value.trim();
|
|
||||||
if (!text && diagPendingFiles.length === 0) return;
|
|
||||||
if (diagPendingFiles.length > 0) sendDiagAttachments();
|
|
||||||
if (text) {
|
|
||||||
addChat('sent', text, 'Gateway direkt');
|
|
||||||
send({ action: 'test_gateway', text });
|
|
||||||
}
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function testRVS() {
|
function testRVS() {
|
||||||
const input = document.getElementById('chat-input');
|
const input = document.getElementById('chat-input');
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
@@ -1771,7 +1756,6 @@
|
|||||||
if (proxy.models && proxy.models.length) showProxyModels(proxy.models);
|
if (proxy.models && proxy.models.length) showProxyModels(proxy.models);
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
document.getElementById('btn-gw').disabled = gw.status !== 'connected';
|
|
||||||
document.getElementById('btn-rvs').disabled = rvs.status !== 'connected';
|
document.getElementById('btn-rvs').disabled = rvs.status !== 'connected';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2094,14 +2078,6 @@
|
|||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function testGatewayFS() {
|
|
||||||
const input = document.getElementById('chat-input-fs');
|
|
||||||
const text = input.value.trim();
|
|
||||||
if (!text) return;
|
|
||||||
addChat('sent', text, 'Gateway direkt');
|
|
||||||
send({ action: 'test_gateway', text });
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
function testRVSFS() {
|
function testRVSFS() {
|
||||||
const input = document.getElementById('chat-input-fs');
|
const input = document.getElementById('chat-input-fs');
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
|
|||||||
+17
-15
@@ -492,9 +492,10 @@ function handleGatewayMessage(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendToGateway(text, isTrace) {
|
function sendToGateway(text, isTrace) {
|
||||||
|
// OpenClaw-Gateway ist raus — Brain via Bridge via RVS ist die einzige
|
||||||
|
// Route. Wir loggen nichts mehr; alte Trace-Aufrufe schliessen wir clean.
|
||||||
if (!gatewayWs || gatewayWs.readyState !== WebSocket.OPEN) {
|
if (!gatewayWs || gatewayWs.readyState !== WebSocket.OPEN) {
|
||||||
log("error", "gateway", "Nicht verbunden — kann nicht senden");
|
if (isTrace) traceEnd(false, "Gateway deprecated — nutze RVS");
|
||||||
if (isTrace) traceEnd(false, "Gateway nicht verbunden");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,22 +758,20 @@ function sendToRVS_raw(msgObj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendToRVS(text, isTrace) {
|
function sendToRVS(text, isTrace) {
|
||||||
// Ueber Gateway senden (zuverlaessig) UND an RVS fuer App-Sichtbarkeit
|
// Brain-Pipeline: Diagnostic → RVS → Bridge → Brain (HTTP). OpenClaw-
|
||||||
// Die Bridge empfaengt RVS-Nachrichten von der App zuverlaessig,
|
// Gateway-Pfad ist abgeschaltet. Sender 'diagnostic' damit die Bridge
|
||||||
// aber die Diagnostic→RVS→Bridge Route hat Zombie-Probleme.
|
// den Text als User-Nachricht ans Brain weiterleitet und die App +
|
||||||
// Deshalb: Gateway fuer ARIA, RVS nur fuer App-Anzeige.
|
// Diagnostic die Bubble live spiegeln koennen.
|
||||||
|
if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) {
|
||||||
// 1. An Gateway senden (damit ARIA antwortet)
|
if (isTrace) traceEnd(false, "RVS nicht verbunden");
|
||||||
const gatewayOk = sendToGateway(text, isTrace);
|
return false;
|
||||||
|
}
|
||||||
// 2. An RVS senden (damit die App die Nachricht sieht)
|
|
||||||
sendToRVS_raw({
|
sendToRVS_raw({
|
||||||
type: "chat",
|
type: "chat",
|
||||||
payload: { text, sender: "diagnostic" },
|
payload: { text, sender: "diagnostic" },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
return gatewayOk;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Claude Proxy Test ────────────────────────────────────
|
// ── Claude Proxy Test ────────────────────────────────────
|
||||||
@@ -1836,8 +1835,11 @@ wss.on("connection", (ws) => {
|
|||||||
const msg = JSON.parse(raw.toString());
|
const msg = JSON.parse(raw.toString());
|
||||||
|
|
||||||
if (msg.action === "test_gateway") {
|
if (msg.action === "test_gateway") {
|
||||||
traceStart("Gateway", msg.text || "aria lebst du noch?");
|
// Deprecated — Gateway-Pfad ist raus. Wir leiten an RVS um damit
|
||||||
sendToGateway(msg.text || "aria lebst du noch?", true);
|
// alte Browser-Sessions die noch den Button anzeigen nicht stumm
|
||||||
|
// ins Leere klicken. Neue Versionen kennen den Button nicht mehr.
|
||||||
|
traceStart("RVS", msg.text || "aria lebst du noch?");
|
||||||
|
sendToRVS(msg.text || "aria lebst du noch?", true);
|
||||||
} else if (msg.action === "test_rvs") {
|
} else if (msg.action === "test_rvs") {
|
||||||
traceStart("RVS", msg.text || "aria lebst du noch?");
|
traceStart("RVS", msg.text || "aria lebst du noch?");
|
||||||
sendToRVS(msg.text || "aria lebst du noch?", true);
|
sendToRVS(msg.text || "aria lebst du noch?", true);
|
||||||
|
|||||||
Reference in New Issue
Block a user