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>
68 lines
2.9 KiB
TypeScript
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';
|
|
}
|