chahinebrini 63fae25531 fix(android-protection): explicit specialUse FGS type — Samsung/Android 16 crash loop
RebreakVpnService.onStartCommand crashed with SecurityException because Android 16's validateForegroundServiceType rejects the implicit 2-arg startForeground(). Now passes FOREGROUND_SERVICE_TYPE_SPECIAL_USE explicitly (Google's documented best practice) and guards the call so a failed foreground promotion stops the service cleanly instead of crashing the app. Verified vs reported Galaxy A54 / Android 16 signature (97% of crash events, 1-user crash loop).

Bundles pending working-tree work across native/marketing/locales/mac + graphify-out rebuild. gitignore: google-services.json + /screenshots/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:33:28 +02:00

68 lines
2.9 KiB
TypeScript

// Parser für Lyras LLM-JSON-Antworten + Emotion-Detection.
import { EMPATHY_RE, HAPPY_RE, JOY_RE, SAD_RE, CONFUSION_RE } from './sosConstants';
import type { Emotion } from '../components/RiveAvatar';
export type LyraEmotion = Emotion;
export type ChipSpec = { label: string; action: string };
// Parse LLM JSON response — robust gegen Markdown-Fences UND abgeschnittenes JSON
export function parseLyraResponse(raw: string): { message: string; chips: ChipSpec[] } {
if (!raw) return { message: '', chips: [] };
// Strip ALL markdown fences (auch wenn nur am Anfang)
const text = raw.trim()
.replace(/^```(?:json)?\s*/i, '')
.replace(/```\s*$/i, '')
.trim();
const start = text.indexOf('{');
if (start === -1) return { message: raw.trim(), chips: [] };
// Erst echtes JSON-Parse versuchen (komplett)
const end = text.lastIndexOf('}');
if (end > start) {
try {
const obj = JSON.parse(text.slice(start, end + 1));
const message = typeof obj.message === 'string' ? obj.message.trim() : '';
const chipsRaw = Array.isArray(obj.chips) ? obj.chips : [];
const chips: ChipSpec[] = chipsRaw
.filter((c: { label?: unknown; action?: unknown }) => c && typeof c.label === 'string' && typeof c.action === 'string')
.slice(0, 5)
.map((c: { label: string; action: string }) => ({ label: c.label.trim(), action: c.action.trim() }));
if (message) return { message, chips };
} catch {/* fall through to recovery */}
}
// RECOVERY: JSON ist abgeschnitten (z.B. max_tokens hit).
// 1) message-Feld per Regex extrahieren
const msgMatch = text.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"/);
let message = '';
if (msgMatch) {
try { message = JSON.parse('"' + msgMatch[1] + '"'); } catch { message = msgMatch[1]; }
}
// 2) Chips: alle vollständigen {label,action}-Objekte einsammeln
const chips: ChipSpec[] = [];
const chipRe = /\{\s*"label"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"action"\s*:\s*"((?:[^"\\]|\\.)*)"\s*\}/g;
let m: RegExpExecArray | null;
while ((m = chipRe.exec(text)) !== null && chips.length < 5) {
try {
chips.push({
label: JSON.parse('"' + m[1] + '"').trim(),
action: JSON.parse('"' + m[2] + '"').trim(),
});
} catch {
chips.push({ label: m[1].trim(), action: m[2].trim() });
}
}
if (message) return { message, chips };
return { message: raw.trim(), chips: [] };
}
export function detectEmotion(text: string): LyraEmotion {
// Reihenfolge = Priorität. joy (große Feier) vor happy (Alltag);
// sad (Verlust/Rückfall/Scham, spiegelnd) vor empathy (allg. Schwere);
// confusion (Rückfrage) zuletzt, da neutral-tonig.
if (JOY_RE.test(text)) return 'joy';
if (HAPPY_RE.test(text)) return 'happy';
if (SAD_RE.test(text)) return 'sad';
if (EMPATHY_RE.test(text)) return 'empathy';
if (CONFUSION_RE.test(text)) return 'confusion';
return 'idle';
}