Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e05c66baa | |||
| 4082a6bf2a | |||
| 3485642b3e | |||
| 1240ae3829 | |||
| 2dd4d38dce | |||
| 7f862ce1f4 | |||
| 528fe97b59 | |||
| 3483d1bfce | |||
| 158423c155 | |||
| 087e91dca1 | |||
| 2de4cbc00f | |||
| 03fc465057 | |||
| b696b47feb | |||
| 6aae565541 | |||
| 214bd218a0 | |||
| 2afeee29ee | |||
| c8dee4c416 | |||
| f49f3c3b08 | |||
| c4bbb06710 | |||
| 4411cc4fff |
@@ -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 10100
|
versionCode 10106
|
||||||
versionName "0.1.1.0"
|
versionName "0.1.1.6"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.1.0",
|
"version": "0.1.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
@@ -10,31 +10,32 @@
|
|||||||
"build:apk": "cd android && ./gradlew assembleRelease"
|
"build:apk": "cd android && ./gradlew assembleRelease"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "18.2.0",
|
||||||
"react-native": "0.73.4",
|
"react-native": "0.73.4",
|
||||||
"@react-navigation/native": "^6.1.9",
|
"react-native-audio-recorder-player": "^3.6.7",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.11",
|
"react-native-camera-kit": "^13.0.0",
|
||||||
"react-native-screens": "3.27.0",
|
|
||||||
"react-native-safe-area-context": "^4.8.2",
|
|
||||||
"react-native-document-picker": "^9.1.1",
|
"react-native-document-picker": "^9.1.1",
|
||||||
"react-native-sound": "^0.11.2",
|
"react-native-fs": "^2.20.0",
|
||||||
"@react-native-community/geolocation": "^3.2.1",
|
|
||||||
"react-native-image-picker": "^7.1.0",
|
"react-native-image-picker": "^7.1.0",
|
||||||
"react-native-permissions": "^4.1.4",
|
"react-native-permissions": "^4.1.4",
|
||||||
"react-native-camera-kit": "^13.0.0",
|
"react-native-safe-area-context": "^4.8.2",
|
||||||
"@react-native-async-storage/async-storage": "^1.21.0",
|
"react-native-screens": "3.27.0",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-sound": "^0.11.2",
|
||||||
"react-native-audio-recorder-player": "^3.6.7"
|
"react-native-svg": "^14.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": "^18.2.48",
|
||||||
"@types/react-native": "^0.73.0",
|
"@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",
|
"jest": "^29.7.0",
|
||||||
"@types/jest": "^29.5.11"
|
"metro-react-native-babel-preset": "^0.77.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,88 @@
|
|||||||
/**
|
/**
|
||||||
* 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
|
* - 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
|
* automatisch klickbar) und ein einzelnes <Text selectable> ohne nested
|
||||||
* <Text> mit eigenem onPress. Nested Text mit onPress fingen die Long-Press-
|
* <Text> mit eigenem onPress — Nested Text mit onPress fing die Long-Press-
|
||||||
* Geste ab, damit war Markieren+Kopieren defekt.
|
* Geste ab, damit war Markieren+Kopieren defekt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Text, TextStyle, StyleProp } from 'react-native';
|
import { View, Text, Image, TextStyle, StyleProp } from 'react-native';
|
||||||
|
import { SvgUri } from 'react-native-svg';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
style?: StyleProp<TextStyle>;
|
style?: StyleProp<TextStyle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<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 MessageText: React.FC<Props> = ({ text, style }) => {
|
||||||
|
const imageUrls = extractImageUrls(text || '');
|
||||||
|
if (imageUrls.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Text style={style} selectable dataDetectorType="all">
|
<Text style={style} selectable dataDetectorType="all">
|
||||||
{text}
|
{text}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text style={style} selectable dataDetectorType="all">
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
{imageUrls.map(u => <InlineImage key={u} uri={u} />)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MessageText;
|
export default MessageText;
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ import {
|
|||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
AppState,
|
AppState,
|
||||||
NativeModules,
|
NativeModules,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
|
import { SvgUri } from 'react-native-svg';
|
||||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
import wakeWordService from '../services/wakeword';
|
import wakeWordService from '../services/wakeword';
|
||||||
@@ -113,7 +115,9 @@ const ChatImage: React.FC<{
|
|||||||
onError: () => void;
|
onError: () => void;
|
||||||
}> = ({ uri, onPress, onError }) => {
|
}> = ({ uri, onPress, onError }) => {
|
||||||
const [aspectRatio, setAspectRatio] = useState<number>(4 / 3);
|
const [aspectRatio, setAspectRatio] = useState<number>(4 / 3);
|
||||||
|
const isSvg = /\.svg(?:\?|$)/i.test(uri);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isSvg) return; // SvgUri hat kein getSize
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
Image.getSize(uri, (w, h) => {
|
Image.getSize(uri, (w, h) => {
|
||||||
if (!cancelled && w > 0 && h > 0) {
|
if (!cancelled && w > 0 && h > 0) {
|
||||||
@@ -124,7 +128,16 @@ const ChatImage: React.FC<{
|
|||||||
}
|
}
|
||||||
}, () => {});
|
}, () => {});
|
||||||
return () => { cancelled = true; };
|
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 (
|
return (
|
||||||
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
|
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
|
||||||
<Image
|
<Image
|
||||||
@@ -1237,10 +1250,30 @@ const ChatScreen: React.FC = () => {
|
|||||||
? '\u270D\uFE0F ARIA schreibt...'
|
? '\u270D\uFE0F ARIA schreibt...'
|
||||||
: '\uD83D\uDCAD ARIA denkt...'}
|
: '\uD83D\uDCAD ARIA denkt...'}
|
||||||
</Text>
|
</Text>
|
||||||
|
<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'}]}>{'🔧'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.thinkingCancel, {borderColor: '#FF3B30'}]}
|
||||||
|
onPress={() => {
|
||||||
|
Alert.alert(
|
||||||
|
'ARIA hart neu starten?',
|
||||||
|
'Container-Restart (~15s). Laufende Anfragen gehen verloren.',
|
||||||
|
[
|
||||||
|
{ text: 'Abbrechen', style: 'cancel' },
|
||||||
|
{ text: 'Neu starten', style: 'destructive', onPress: () => rvs.send('aria_restart' as any, {}) },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.thinkingCancelText, {color: '#FF3B30'}]}>{'🚨'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.thinkingCancel} onPress={cancelRequest}>
|
<TouchableOpacity style={styles.thinkingCancel} onPress={cancelRequest}>
|
||||||
<Text style={styles.thinkingCancelText}>Abbrechen</Text>
|
<Text style={styles.thinkingCancelText}>Abbrechen</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pending Anhaenge Vorschau */}
|
{/* Pending Anhaenge Vorschau */}
|
||||||
@@ -1351,11 +1384,17 @@ const ChatScreen: React.FC = () => {
|
|||||||
onPress={() => setFullscreenImage(null)}
|
onPress={() => setFullscreenImage(null)}
|
||||||
>
|
>
|
||||||
{fullscreenImage && (
|
{fullscreenImage && (
|
||||||
|
/\.svg(?:\?|$)/i.test(fullscreenImage) ? (
|
||||||
|
<View style={styles.fullscreenImage}>
|
||||||
|
<SvgUri uri={fullscreenImage} width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: fullscreenImage }}
|
source={{ uri: fullscreenImage }}
|
||||||
style={styles.fullscreenImage}
|
style={styles.fullscreenImage}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1288,6 +1288,74 @@ const SettingsScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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>
|
||||||
|
<Text style={[styles.toggleHint, {marginTop: 12}]}>
|
||||||
|
Wenn auch Reparieren nicht hilft — Container hart neu starten.
|
||||||
|
ARIA ist dann ~15 Sekunden weg und kommt mit frischem State zurueck.
|
||||||
|
Laufende Anfragen gehen verloren.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,59,48,0.15)'}]}
|
||||||
|
onPress={() => {
|
||||||
|
Alert.alert(
|
||||||
|
'ARIA hart neu starten?',
|
||||||
|
'Container-Restart (~15s). Laufende Anfragen gehen verloren.',
|
||||||
|
[
|
||||||
|
{ text: 'Abbrechen', style: 'cancel' },
|
||||||
|
{ text: 'Neu starten', style: 'destructive', onPress: () => {
|
||||||
|
rvs.send('aria_restart' as any, {});
|
||||||
|
ToastAndroid.show('Container-Restart angestossen…', ToastAndroid.LONG);
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.clearButtonText, {color: '#FF3B30'}]}>{'🚨 ARIA hart neu starten'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={[styles.toggleHint, {marginTop: 12}]}>
|
||||||
|
Konversation komplett zuruecksetzen — alle bisherigen Nachrichten
|
||||||
|
aus ARIA's Session loeschen + Container neu. Anders als der harte
|
||||||
|
Restart wird hier auch ARIA's Erinnerung an die laufende
|
||||||
|
Konversation gewipt. Geschieht automatisch alle 140 Nachrichten
|
||||||
|
(Bridge-Setting COMPACT_AFTER_MESSAGES).
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,149,0,0.15)'}]}
|
||||||
|
onPress={() => {
|
||||||
|
Alert.alert(
|
||||||
|
'Konversation komprimieren?',
|
||||||
|
'Alle Nachrichten in ARIAs aktueller Session werden geloescht und der Container neu gestartet. ARIA vergisst den bisherigen Gespraechsverlauf.',
|
||||||
|
[
|
||||||
|
{ text: 'Abbrechen', style: 'cancel' },
|
||||||
|
{ text: 'Komprimieren', style: 'destructive', onPress: () => {
|
||||||
|
rvs.send('aria_session_reset' as any, {});
|
||||||
|
ToastAndroid.show('Session wird zurueckgesetzt…', ToastAndroid.LONG);
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.clearButtonText, {color: '#FF9500'}]}>{'🧹 Konversation komprimieren'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
{/* === Logs === */}
|
{/* === Logs === */}
|
||||||
|
|||||||
+36
-13
@@ -52,11 +52,21 @@ Fuer Web-Anfragen: **WebFetch** oder **Bash mit curl**. Niemals sagen "ich habe
|
|||||||
4. **Regelmaessig committen** — mit sinnvollen Commit-Messages.
|
4. **Regelmaessig committen** — mit sinnvollen Commit-Messages.
|
||||||
5. **Tageslog fuehren** — was wurde getan, was ist offen.
|
5. **Tageslog fuehren** — was wurde getan, was ist offen.
|
||||||
|
|
||||||
## Dateien an Stefan zurueckgeben
|
## Dateien an Stefan zurueckgeben — KRITISCH
|
||||||
|
|
||||||
Wenn du eine Datei fuer Stefan erstellt hast (PDF, Bild, ausgefuelltes
|
**Das ist die EINZIGE Methode wie Stefan an Dateien rankommt. Ohne
|
||||||
Formular, Markdown, CSV, ZIP, ...), speichere sie unter `/shared/uploads/`
|
diese Schritte sieht und bekommt er die Datei NICHT.**
|
||||||
mit `aria_`-Prefix:
|
|
||||||
|
### Regel 1 — Speicher-Ort
|
||||||
|
|
||||||
|
Dateien fuer Stefan AUSSCHLIESSLICH unter `/shared/uploads/` speichern.
|
||||||
|
|
||||||
|
NIEMALS in:
|
||||||
|
- `/home/node/.openclaw/workspace/...` (das ist NUR dein Arbeitsverzeichnis,
|
||||||
|
Stefan hat keinen Zugriff darauf)
|
||||||
|
- `/tmp/...`, `/root/...`, oder sonst irgendwo
|
||||||
|
|
||||||
|
Dateinamen mit `aria_`-Prefix damit Cleanup-Scripts sie zuordnen koennen:
|
||||||
|
|
||||||
```
|
```
|
||||||
/shared/uploads/aria_<beschreibender_name>.<ext>
|
/shared/uploads/aria_<beschreibender_name>.<ext>
|
||||||
@@ -65,22 +75,35 @@ mit `aria_`-Prefix:
|
|||||||
Beispiele: `aria_termin_zusage.pdf`, `aria_einkaufsliste.md`,
|
Beispiele: `aria_termin_zusage.pdf`, `aria_einkaufsliste.md`,
|
||||||
`aria_logs_2026-05-10.zip`.
|
`aria_logs_2026-05-10.zip`.
|
||||||
|
|
||||||
Haenge dann am Ende deiner Antwort EINMALIG den Marker an:
|
### Regel 2 — Marker im Antworttext
|
||||||
|
|
||||||
|
Am Ende deiner Antwort EINMALIG den Marker setzen:
|
||||||
|
|
||||||
```
|
```
|
||||||
[FILE: /shared/uploads/aria_<name>.<ext>]
|
[FILE: /shared/uploads/aria_<name>.<ext>]
|
||||||
```
|
```
|
||||||
|
|
||||||
Der Marker wird automatisch aus dem sichtbaren Antworttext entfernt
|
OHNE diesen Marker erscheint die Datei NICHT in der App / Diagnostic.
|
||||||
(TTS liest ihn nicht vor) und als Datei-Anhang in der App und im
|
|
||||||
Diagnostic-Chat angezeigt — Stefan kann die Datei mit einem Klick
|
|
||||||
oeffnen (PDF-Viewer, Bildbetrachter, Standard-App per MIME-Type).
|
|
||||||
|
|
||||||
Mehrere Dateien in einer Antwort: einfach mehrere `[FILE: ...]`-Marker
|
Mehrere Dateien: mehrere `[FILE: ...]`-Marker am Ende, jeder in
|
||||||
am Ende, jeder in einer eigenen Zeile.
|
eigener Zeile.
|
||||||
|
|
||||||
Pfad muss zwingend mit `/shared/uploads/` beginnen — andere Pfade
|
### Beispiel — kompletter Workflow
|
||||||
werden ignoriert.
|
|
||||||
|
User: "Schreib mir ein Lasagne-Rezept als md-Datei"
|
||||||
|
|
||||||
|
1. Du schreibst die Datei: `Write` Tool mit Pfad `/shared/uploads/aria_lasagne.md`
|
||||||
|
2. Antwort an Stefan:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hier dein Lasagne-Rezept — Ragu am Vortag, echter Parmesan,
|
||||||
|
Ruhezeit nicht skippen. Beim Schichten Bechamel auf jede Lage.
|
||||||
|
|
||||||
|
[FILE: /shared/uploads/aria_lasagne.md]
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Marker wird automatisch aus dem sichtbaren Text entfernt und
|
||||||
|
als Anhang-Bubble angezeigt. Stefan tippt drauf → oeffnet die Datei.
|
||||||
|
|
||||||
## Stimme
|
## Stimme
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,89 @@ Wenn ein Tool nicht klappt, probiere die Alternative. Niemals sagen "ich habe ke
|
|||||||
- Destruktive Operationen (Dateien loeschen, Datenbanken droppen)
|
- Destruktive Operationen (Dateien loeschen, Datenbanken droppen)
|
||||||
- Push auf main
|
- Push auf main
|
||||||
|
|
||||||
|
## Dateien an Stefan zurueckgeben — KRITISCH
|
||||||
|
|
||||||
|
**Das ist die EINZIGE Methode wie Stefan an Dateien rankommt. Ohne diese
|
||||||
|
Schritte sieht und bekommt er die Datei NICHT.**
|
||||||
|
|
||||||
|
### Regel 1 — Speicher-Ort
|
||||||
|
|
||||||
|
Dateien fuer Stefan AUSSCHLIESSLICH unter `/shared/uploads/` speichern.
|
||||||
|
|
||||||
|
NIEMALS in:
|
||||||
|
- `/home/node/.openclaw/workspace/...` (NUR dein Arbeitsverzeichnis,
|
||||||
|
Stefan hat keinen Zugriff)
|
||||||
|
- `/tmp/...`, `/root/...`, oder sonst irgendwo
|
||||||
|
|
||||||
|
Dateinamen mit `aria_`-Prefix:
|
||||||
|
|
||||||
|
```
|
||||||
|
/shared/uploads/aria_<beschreibender_name>.<ext>
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiele: `aria_termin_zusage.pdf`, `aria_einkaufsliste.md`,
|
||||||
|
`aria_logs_2026-05-10.zip`.
|
||||||
|
|
||||||
|
### Regel 2 — Marker im Antworttext
|
||||||
|
|
||||||
|
Am Ende deiner Antwort EINMALIG den Marker setzen:
|
||||||
|
|
||||||
|
```
|
||||||
|
[FILE: /shared/uploads/aria_<name>.<ext>]
|
||||||
|
```
|
||||||
|
|
||||||
|
OHNE diesen Marker erscheint die Datei NICHT in der App / Diagnostic.
|
||||||
|
|
||||||
|
Mehrere Dateien: mehrere `[FILE: ...]`-Marker am Ende, jeder in
|
||||||
|
eigener Zeile.
|
||||||
|
|
||||||
|
### Beispiel — kompletter Workflow
|
||||||
|
|
||||||
|
User: "Schreib mir ein Lasagne-Rezept als md-Datei"
|
||||||
|
|
||||||
|
1. Du schreibst: `Write` Tool mit Pfad `/shared/uploads/aria_lasagne.md`
|
||||||
|
2. Antwort an Stefan:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hier dein Lasagne-Rezept — Ragu am Vortag, echter Parmesan,
|
||||||
|
Ruhezeit nicht skippen. Beim Schichten Bechamel auf jede Lage.
|
||||||
|
|
||||||
|
[FILE: /shared/uploads/aria_lasagne.md]
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
## Stimme
|
||||||
|
|
||||||
TTS laeuft ueber F5-TTS auf der Gamebox (Voice Cloning). Stefan kann
|
TTS laeuft ueber F5-TTS auf der Gamebox (Voice Cloning). Stefan kann
|
||||||
|
|||||||
@@ -549,6 +549,12 @@ class ARIABridge:
|
|||||||
# Beeinflusst das Timeout fuer stt_request — bei "loading" warten wir laenger,
|
# Beeinflusst das Timeout fuer stt_request — bei "loading" warten wir laenger,
|
||||||
# weil das Modell beim ersten Request noch ~1-2 Min runtergeladen werden kann.
|
# weil das Modell beim ersten Request noch ~1-2 Min runtergeladen werden kann.
|
||||||
self._remote_stt_ready: bool = False
|
self._remote_stt_ready: bool = False
|
||||||
|
# User-Message-Counter fuer Auto-Compact. Bei zu langer Konversation
|
||||||
|
# sprengt die argv-Liste beim Claude-Subprocess-Spawn (E2BIG). Bei
|
||||||
|
# COMPACT_AFTER erreicht → Sessions reset + Container restart.
|
||||||
|
# Counter ueberlebt Bridge-Restart nicht (frischer Zaehler beim Start ok).
|
||||||
|
self._user_message_count: int = 0
|
||||||
|
self._compact_after = int(os.getenv("COMPACT_AFTER_MESSAGES", "140"))
|
||||||
# Pending Files: wenn die App ein Bild + Text gleichzeitig schickt, kommen
|
# Pending Files: wenn die App ein Bild + Text gleichzeitig schickt, kommen
|
||||||
# zwei separate RVS-Events ('file' und 'chat') — wir buffern die Files
|
# zwei separate RVS-Events ('file' und 'chat') — wir buffern die Files
|
||||||
# kurz und mergen sie mit dem nachfolgenden Chat-Text zu einer einzigen
|
# kurz und mergen sie mit dem nachfolgenden Chat-Text zu einer einzigen
|
||||||
@@ -1170,12 +1176,53 @@ class ARIABridge:
|
|||||||
await self.send_to_core(text, source="app-file+chat")
|
await self.send_to_core(text, source="app-file+chat")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _trigger_session_reset(self) -> None:
|
||||||
|
"""Sessions loeschen + Container restart via Diagnostic HTTP-API."""
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"http://localhost:3001/api/aria-session-reset",
|
||||||
|
data=b"{}",
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
def _do_reset():
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=45) as resp:
|
||||||
|
return resp.status
|
||||||
|
except Exception as e:
|
||||||
|
return f"err:{e}"
|
||||||
|
result = await asyncio.get_event_loop().run_in_executor(None, _do_reset)
|
||||||
|
logger.info("[core] Session-Reset Result: %s", result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[core] Session-Reset Trigger fehlgeschlagen: %s", e)
|
||||||
|
|
||||||
async def send_to_core(self, text: str, source: str = "bridge") -> None:
|
async def send_to_core(self, text: str, source: str = "bridge") -> None:
|
||||||
"""Sendet Text an aria-core (OpenClaw chat.send Protokoll)."""
|
"""Sendet Text an aria-core (OpenClaw chat.send Protokoll)."""
|
||||||
if self.ws_core is None:
|
if self.ws_core is None:
|
||||||
logger.error("[core] Nicht verbunden — Nachricht verworfen: '%s'", text[:60])
|
logger.error("[core] Nicht verbunden — Nachricht verworfen: '%s'", text[:60])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Auto-Compact: bei zu vielen User-Messages laeuft argv beim Subprocess-
|
||||||
|
# Spawn ueber (E2BIG). Vor send pruefen, ggf. Sessions resetten.
|
||||||
|
if source.startswith("app") and self._compact_after > 0:
|
||||||
|
self._user_message_count += 1
|
||||||
|
if self._user_message_count >= self._compact_after:
|
||||||
|
logger.warning("[core] Auto-Compact: %d Messages erreicht — Session-Reset",
|
||||||
|
self._user_message_count)
|
||||||
|
self._user_message_count = 0
|
||||||
|
# Reset triggern via Diagnostic (asynchron, blockiert send nicht)
|
||||||
|
asyncio.create_task(self._trigger_session_reset())
|
||||||
|
# User informieren — der naechste Request kommt erst nach Restart durch
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "chat",
|
||||||
|
"payload": {
|
||||||
|
"text": f"[Compact] Konversation war lang ({self._compact_after} Nachrichten) — Session wurde geleert, ARIA startet frisch. Deine letzte Nachricht bitte gleich nochmal senden.",
|
||||||
|
"sender": "aria",
|
||||||
|
},
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
# Aktive Session vom Diagnostic holen
|
# Aktive Session vom Diagnostic holen
|
||||||
self._fetch_active_session()
|
self._fetch_active_session()
|
||||||
|
|
||||||
@@ -1580,6 +1627,76 @@ class ARIABridge:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("[rvs] file_saved konnte nicht an App gesendet werden: %s", e)
|
logger.warning("[rvs] file_saved konnte nicht an App gesendet werden: %s", e)
|
||||||
|
|
||||||
|
elif msg_type == "aria_session_reset":
|
||||||
|
# Manueller Compact-Trigger: Sessions weg + Restart
|
||||||
|
logger.warning("[rvs] aria_session_reset Request von App")
|
||||||
|
self._user_message_count = 0
|
||||||
|
asyncio.create_task(self._trigger_session_reset())
|
||||||
|
return
|
||||||
|
|
||||||
|
elif msg_type == "aria_restart":
|
||||||
|
# App-Button "ARIA hart neu starten" → docker restart aria-core
|
||||||
|
# via Diagnostic (der hat den Docker-Socket gemountet).
|
||||||
|
logger.warning("[rvs] aria_restart Request von App — harter Container-Restart")
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"http://localhost:3001/api/aria-restart",
|
||||||
|
data=b"{}",
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
def _do_restart():
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=45) 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_restart)
|
||||||
|
logger.info("[rvs] aria_restart Result: status=%s", status)
|
||||||
|
# Note: bei erfolgreichem Restart ist die RVS-Verbindung sehr
|
||||||
|
# wahrscheinlich kurz weg (aria-bridge ist im service:aria-Network).
|
||||||
|
# Die Antwort kommt evtl. nicht mehr durch — egal.
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[rvs] aria_restart Weiterleitung fehlgeschlagen: %s", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
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":
|
elif msg_type == "file_request":
|
||||||
# App fordert eine Datei an (Re-Download nach Cache-Leerung)
|
# App fordert eine Datei an (Re-Download nach Cache-Leerung)
|
||||||
server_path = payload.get("serverPath", "")
|
server_path = payload.get("serverPath", "")
|
||||||
|
|||||||
+53
-1
@@ -288,8 +288,13 @@
|
|||||||
<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;align-items:center;justify-content:space-between;">
|
<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>
|
<span><span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text">ARIA denkt...</span></span>
|
||||||
|
<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="ariaSessionReset()" style="padding:2px 10px;font-size:11px;color:#FF9500;border-color:#FF9500;" title="Konversation komprimieren — Sessions weg + Restart">🧹 Compact</button>
|
||||||
|
<button class="btn secondary" onclick="ariaRestart()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;" title="Container hart neu starten (~15s)">🚨 Hart neu</button>
|
||||||
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
||||||
</div>
|
</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 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>
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
@@ -993,7 +998,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === 'chat_final') {
|
if (msg.type === 'chat_final') {
|
||||||
addChat('received', msg.text, 'chat:final');
|
// [FILE: /shared/uploads/aria_xxx.ext]-Marker aus dem Antworttext
|
||||||
|
// entfernen — die Datei kommt separat via file_from_aria.
|
||||||
|
// (Diagnostic empfaengt chat_final direkt vom Gateway, Bridge
|
||||||
|
// hat darum nicht filtern koennen.)
|
||||||
|
const cleaned = (msg.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
|
||||||
|
addChat('received', cleaned, 'chat:final');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.type === 'file_from_aria') {
|
if (msg.type === 'file_from_aria') {
|
||||||
@@ -1503,6 +1513,7 @@
|
|||||||
`<a ${linkAttrs} style="color:#0096FF;text-decoration:underline;">${escapeHtml(name)}</a>` +
|
`<a ${linkAttrs} style="color:#0096FF;text-decoration:underline;">${escapeHtml(name)}</a>` +
|
||||||
` <span style="color:#888;font-size:11px;">(${escapeHtml(mimeType)}, ${sizeStr})</span>` +
|
` <span style="color:#888;font-size:11px;">(${escapeHtml(mimeType)}, ${sizeStr})</span>` +
|
||||||
preview +
|
preview +
|
||||||
|
`<div style="margin-top:4px;font-size:10px;color:#666;font-family:monospace;">${escapeHtml(serverPath)}</div>` +
|
||||||
`<div class="meta">ARIA-Datei — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
`<div class="meta">ARIA-Datei — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
||||||
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;
|
||||||
@@ -1852,6 +1863,47 @@
|
|||||||
renderDiagPending();
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hard-Restart — docker restart aria-core ──────
|
||||||
|
function ariaRestart() {
|
||||||
|
if (!confirm('ARIA wird hart neu gestartet (Container-Restart, ~15s).\n\nLaufende Anfragen gehen verloren. Sicher?')) return;
|
||||||
|
fetch('/api/aria-restart', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.ok) {
|
||||||
|
addLog('info', 'server', 'ARIA neu gestartet — wartet auf Reconnect');
|
||||||
|
} else {
|
||||||
|
addLog('error', 'server', 'Restart fehlgeschlagen: ' + (data.error || ''));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => addLog('error', 'server', 'Restart Request fehlgeschlagen: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Compact / Session-Reset ──────
|
||||||
|
function ariaSessionReset() {
|
||||||
|
if (!confirm('Konversation komprimieren: alle Nachrichten in ARIAs aktueller Session werden geloescht und der Container neu gestartet. ARIA vergisst den bisherigen Gespraechsverlauf. Sicher?')) return;
|
||||||
|
fetch('/api/aria-session-reset', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.ok) addLog('info', 'server', 'Session geleert, ARIA neu gestartet');
|
||||||
|
else addLog('error', 'server', 'Reset fehlgeschlagen: ' + (data.error || ''));
|
||||||
|
})
|
||||||
|
.catch(err => addLog('error', 'server', 'Reset Request fehlgeschlagen: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Abbrechen ──────────────────────────────
|
// ── Abbrechen ──────────────────────────────
|
||||||
function cancelRequest() {
|
function cancelRequest() {
|
||||||
send({ action: 'cancel_request' });
|
send({ action: 'cancel_request' });
|
||||||
|
|||||||
@@ -1342,6 +1342,97 @@ const server = http.createServer((req, res) => {
|
|||||||
dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
|
dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
res.end(JSON.stringify({ ok: true }));
|
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 === "/api/aria-session-reset" && req.method === "POST") {
|
||||||
|
// Sessions weg + Container neu — fuer Compact-After-N-Messages.
|
||||||
|
// E2BIG bei zu langen Sessions: argv beim Subprocess-spawn ueberschritten.
|
||||||
|
log("warn", "server", "HTTP /api/aria-session-reset — Sessions loeschen + Restart");
|
||||||
|
broadcast({ type: "watchdog", status: "fixing", message: "Sessions werden geleert — ARIA bekommt frischen Start" });
|
||||||
|
dockerExec("aria-core", "rm -f /home/node/.openclaw/agents/main/sessions/*.jsonl /home/node/.openclaw/agents/main/sessions/*.lock 2>&1 && echo '{}' > /home/node/.openclaw/agents/main/sessions/sessions.json")
|
||||||
|
.then(() => {
|
||||||
|
// Restart via Docker-API (gleicher Pfad wie /api/aria-restart)
|
||||||
|
const restartReq = http.request({
|
||||||
|
socketPath: "/var/run/docker.sock",
|
||||||
|
path: "/containers/aria-core/restart?t=10",
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Length": 0 },
|
||||||
|
timeout: 30000,
|
||||||
|
}, (dRes) => {
|
||||||
|
if (dRes.statusCode === 204) {
|
||||||
|
log("info", "server", "aria-session-reset OK");
|
||||||
|
broadcast({ type: "watchdog", status: "fixed", message: "Sessions geleert, ARIA neu gestartet" });
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: true }));
|
||||||
|
} else {
|
||||||
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: `Docker-API ${dRes.statusCode}` }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
restartReq.on("error", (err) => {
|
||||||
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||||
|
});
|
||||||
|
restartReq.end();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log("error", "server", `aria-session-reset Cleanup fehlgeschlagen: ${err.message}`);
|
||||||
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (req.url === "/api/aria-restart" && req.method === "POST") {
|
||||||
|
// Harter Restart — fuer Faelle wo doctor --fix nicht reicht (alive aber
|
||||||
|
// haengender Run). Geht ueber Docker-API (Socket), kein CLI noetig.
|
||||||
|
// POST /containers/aria-core/restart?t=10 → SIGTERM, dann nach 10s SIGKILL.
|
||||||
|
log("warn", "server", "HTTP /api/aria-restart — harter Container-Restart");
|
||||||
|
broadcast({ type: "watchdog", status: "fixing", message: "ARIA wird hart neu gestartet (~15s)" });
|
||||||
|
const restartReq = http.request({
|
||||||
|
socketPath: "/var/run/docker.sock",
|
||||||
|
path: "/containers/aria-core/restart?t=10",
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Length": 0 },
|
||||||
|
timeout: 30000,
|
||||||
|
}, (dRes) => {
|
||||||
|
let body = "";
|
||||||
|
dRes.on("data", (c) => body += c);
|
||||||
|
dRes.on("end", () => {
|
||||||
|
// Docker-API: 204 = OK, 404 = container nicht da, 500 = anderer Fehler
|
||||||
|
if (dRes.statusCode === 204) {
|
||||||
|
log("info", "server", "aria-restart OK (Docker-API)");
|
||||||
|
broadcast({ type: "watchdog", status: "fixed", message: "ARIA wurde neu gestartet — sollte gleich antworten" });
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: true }));
|
||||||
|
} else {
|
||||||
|
log("error", "server", `aria-restart Docker-API ${dRes.statusCode}: ${body.slice(0, 200)}`);
|
||||||
|
broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: HTTP ${dRes.statusCode}` });
|
||||||
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: `Docker-API ${dRes.statusCode}: ${body}` }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
restartReq.on("error", (err) => {
|
||||||
|
log("error", "server", `aria-restart Socket-Fehler: ${err.message}`);
|
||||||
|
broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: ${err.message}` });
|
||||||
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||||
|
});
|
||||||
|
restartReq.end();
|
||||||
|
return;
|
||||||
} else if (req.url.startsWith("/shared/")) {
|
} else if (req.url.startsWith("/shared/")) {
|
||||||
// Dateien aus Shared Volume ausliefern (Bilder, Uploads)
|
// Dateien aus Shared Volume ausliefern (Bilder, Uploads)
|
||||||
const filePath = decodeURIComponent(req.url);
|
const filePath = decodeURIComponent(req.url);
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ services:
|
|||||||
- RVS_TLS=${RVS_TLS:-true}
|
- RVS_TLS=${RVS_TLS:-true}
|
||||||
- RVS_TLS_FALLBACK=${RVS_TLS_FALLBACK:-true}
|
- RVS_TLS_FALLBACK=${RVS_TLS_FALLBACK:-true}
|
||||||
- RVS_TOKEN=${RVS_TOKEN:-}
|
- RVS_TOKEN=${RVS_TOKEN:-}
|
||||||
|
- COMPACT_AFTER_MESSAGES=${COMPACT_AFTER_MESSAGES:-140}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ─── Diagnostic (Selbstcheck-UI und Einstellungen) ────
|
# ─── Diagnostic (Selbstcheck-UI und Einstellungen) ────
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# ARIA — Setup-Script
|
||||||
|
#
|
||||||
|
# Materialisiert Config-Dateien aus *.example-Vorlagen wenn
|
||||||
|
# das Original fehlt. Wird einmalig nach git clone und nach
|
||||||
|
# jedem git pull empfohlen — schadet auch sonst nichts (idempotent,
|
||||||
|
# ueberschreibt nichts Bestehendes).
|
||||||
|
#
|
||||||
|
# Beispiele:
|
||||||
|
# aria-data/config/USER.md.example → USER.md (wenn nicht vorhanden)
|
||||||
|
# aria-data/config/aria.env.example → aria.env (wenn nicht vorhanden)
|
||||||
|
#
|
||||||
|
# Diese Files sind via .gitignore vom Repo ausgeschlossen — die
|
||||||
|
# Vorlagen liegen aber im Repo damit ein frisches Setup ohne lange
|
||||||
|
# Anleitung lauffaehig ist.
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
created=0
|
||||||
|
skipped=0
|
||||||
|
|
||||||
|
for example in aria-data/config/*.example; do
|
||||||
|
[ -f "$example" ] || continue
|
||||||
|
target="${example%.example}"
|
||||||
|
if [ -e "$target" ]; then
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
else
|
||||||
|
cp "$example" "$target"
|
||||||
|
echo "✓ $target erstellt aus $(basename "$example")"
|
||||||
|
created=$((created + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $created -eq 0 ]; then
|
||||||
|
echo "Alle Config-Dateien vorhanden ($skipped uebersprungen)."
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "$created Datei(en) angelegt, $skipped uebersprungen."
|
||||||
|
echo "Falls noetig anpassen: aria-data/config/"
|
||||||
|
fi
|
||||||
@@ -19,6 +19,9 @@ const ALLOWED_TYPES = new Set([
|
|||||||
"agent_activity", "cancel_request",
|
"agent_activity", "cancel_request",
|
||||||
"audio_pcm",
|
"audio_pcm",
|
||||||
"file_from_aria",
|
"file_from_aria",
|
||||||
|
"doctor_fix",
|
||||||
|
"aria_restart",
|
||||||
|
"aria_session_reset",
|
||||||
"xtts_delete_voice",
|
"xtts_delete_voice",
|
||||||
"voice_preload", "voice_ready",
|
"voice_preload", "voice_ready",
|
||||||
"stt_request", "stt_response",
|
"stt_request", "stt_response",
|
||||||
|
|||||||
Reference in New Issue
Block a user