Compare commits
6 Commits
v0.1.8.6
...
86f20d3b64
| Author | SHA1 | Date | |
|---|---|---|---|
| 86f20d3b64 | |||
| 78211f09ce | |||
| b2edee9adb | |||
| bb13477ef9 | |||
| 710e7c88d8 | |||
| b6ee5552f0 |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(ssh root@172.0.2.33 \"ls -la /root/ARIA-AGENT/aria-shared/logs/\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 10806
|
versionCode 10808
|
||||||
versionName "0.1.8.6"
|
versionName "0.1.8.8"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.8.6",
|
"version": "0.1.8.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -497,6 +497,49 @@ const SettingsScreen: React.FC = () => {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Datei-Manager: Einzel-Datei-Download. ChatScreen subscribet auch auf
|
||||||
|
// file_response — der versucht aber nur Chat-Bubble-Attachments zu
|
||||||
|
// patchen und macht nix wenn die requestId nicht zu einer Nachricht
|
||||||
|
// passt. Hier behandeln wir die Manager-initiierten Downloads
|
||||||
|
// (requestId-Praefix 'single-' aus bulkDownload). Schreibt nach
|
||||||
|
// ~/Download/ wie der ZIP-Pfad.
|
||||||
|
if (message.type === ('file_response' as any)) {
|
||||||
|
const p: any = message.payload || {};
|
||||||
|
const reqId = (p.requestId as string) || '';
|
||||||
|
if (!reqId.startsWith('single-')) return; // nicht unsere Anfrage
|
||||||
|
if (p.error) {
|
||||||
|
ToastAndroid.show('Download fehlgeschlagen: ' + p.error, ToastAndroid.LONG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const b64 = (p.base64 as string) || '';
|
||||||
|
if (!b64) return;
|
||||||
|
const fileName = (p.name as string) ||
|
||||||
|
(p.serverPath as string || '').split('/').pop() ||
|
||||||
|
'aria-download';
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const dir = RNFS.DownloadDirectoryPath;
|
||||||
|
const filePath = `${dir}/${fileName}`;
|
||||||
|
// Falls Datei schon existiert: Suffix anhaengen damit nichts
|
||||||
|
// ueberschrieben wird.
|
||||||
|
let target = filePath;
|
||||||
|
let i = 1;
|
||||||
|
while (await RNFS.exists(target)) {
|
||||||
|
const dot = fileName.lastIndexOf('.');
|
||||||
|
const base = dot > 0 ? fileName.slice(0, dot) : fileName;
|
||||||
|
const ext = dot > 0 ? fileName.slice(dot) : '';
|
||||||
|
target = `${dir}/${base} (${i})${ext}`;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
await RNFS.writeFile(target, b64, 'base64');
|
||||||
|
const sizeKb = Math.round(((b64.length * 0.75)) / 1024);
|
||||||
|
ToastAndroid.show(`Gespeichert: ${target.split('/').pop()} (${sizeKb} KB)`, ToastAndroid.LONG);
|
||||||
|
} catch (e: any) {
|
||||||
|
ToastAndroid.show('Speichern fehlgeschlagen: ' + e.message, ToastAndroid.LONG);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
// Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen
|
// Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen
|
||||||
if (message.type === ('xtts_voice_saved' as any)) {
|
if (message.type === ('xtts_voice_saved' as any)) {
|
||||||
const name = (message.payload as any).name as string;
|
const name = (message.payload as any).name as string;
|
||||||
|
|||||||
@@ -344,23 +344,38 @@ class WakeWordService {
|
|||||||
/** Konversation beenden — User hat im Window nichts gesagt.
|
/** Konversation beenden — User hat im Window nichts gesagt.
|
||||||
* Mit Wake-Word: zurueck zu 'armed' (Listener wieder an).
|
* Mit Wake-Word: zurueck zu 'armed' (Listener wieder an).
|
||||||
* Ohne: zurueck zu 'off'.
|
* Ohne: zurueck zu 'off'.
|
||||||
|
*
|
||||||
|
* WICHTIG: setzt bargeListening=false BEVOR OpenWakeWord.start() laeuft.
|
||||||
|
* Grund: wenn endConversation aus dem onPlaybackFinished-Handler kommt,
|
||||||
|
* feuert direkt danach ein zweiter Listener (stopBargeListening) — der
|
||||||
|
* wuerde sonst OpenWakeWord.stop() rufen weil bargeListening noch true
|
||||||
|
* ist, und unseren frisch re-armierten Listener killen.
|
||||||
*/
|
*/
|
||||||
async endConversation(): Promise<void> {
|
async endConversation(): Promise<void> {
|
||||||
if (this.state !== 'conversing') {
|
if (this.state !== 'conversing') {
|
||||||
// Nicht in conversing — typ. nach App-Resume bevor Streaming endete.
|
|
||||||
// Trotzdem loggen damit wir's im Diagnostic sehen.
|
|
||||||
import('./logger').then(m => m.reportAppDebug('wake.end',
|
import('./logger').then(m => m.reportAppDebug('wake.end',
|
||||||
`endConversation called but state=${this.state} → noop`)).catch(()=>{});
|
`endConversation called but state=${this.state} → noop`)).catch(()=>{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const wasBarge = this.bargeListening;
|
||||||
|
// Flag NULLEN bevor wir die Listener triggern. Sonst killt der parallele
|
||||||
|
// stopBargeListening-Listener (TTS-end) gleich danach unseren Native-
|
||||||
|
// OpenWakeWord, weil er bargeListening=true sieht und annimmt er muss
|
||||||
|
// den Listener stoppen.
|
||||||
|
this.bargeListening = false;
|
||||||
import('./logger').then(m => m.reportAppDebug('wake.end',
|
import('./logger').then(m => m.reportAppDebug('wake.end',
|
||||||
`endConversation called, nativeReady=${this.nativeReady}, calling OpenWakeWord.start()`)).catch(()=>{});
|
`endConversation called, wasBarge=${wasBarge}, nativeReady=${this.nativeReady}`)).catch(()=>{});
|
||||||
if (this.nativeReady && OpenWakeWord) {
|
if (this.nativeReady && OpenWakeWord) {
|
||||||
|
// Wenn wakeword schon laeuft (war Barge-Listener waehrend TTS):
|
||||||
|
// OpenWakeWord.start() ist idempotent (Kotlin checkt running.get()
|
||||||
|
// und resolved sofort). Wir koennen es trotzdem rufen — billiger
|
||||||
|
// als state extra zu fragen, garantiert dass nach diesem Pfad
|
||||||
|
// Native auch wirklich an ist falls es out-of-band gestoppt wurde.
|
||||||
try {
|
try {
|
||||||
await OpenWakeWord.start();
|
await OpenWakeWord.start();
|
||||||
console.log('[WakeWord] Konversation zu Ende — zurueck zu armed');
|
console.log('[WakeWord] Konversation zu Ende — zurueck zu armed (wasBarge=%s)', wasBarge);
|
||||||
import('./logger').then(m => m.reportAppDebug('wake.end',
|
import('./logger').then(m => m.reportAppDebug('wake.end',
|
||||||
`OpenWakeWord.start() OK → state=armed, keyword=${this.keyword}`)).catch(()=>{});
|
`OpenWakeWord.start() OK → state=armed, wasBarge=${wasBarge}`)).catch(()=>{});
|
||||||
ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
|
ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
|
||||||
this.setState('armed');
|
this.setState('armed');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -602,6 +602,61 @@ SEED_RULES: List[dict] = [
|
|||||||
"'API Key' im Auth-Kapitel). Nicht raten."
|
"'API Key' im Auth-Kapitel). Nicht raten."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"migration_key": "seed/skill-rule/list-api-pagination-snapshot",
|
||||||
|
"type": "rule",
|
||||||
|
"title": "Listen-API: einmal vollstaendig laden, DANN entscheiden",
|
||||||
|
"category": "verhalten",
|
||||||
|
"content": (
|
||||||
|
"Wenn ein Tool-Resultat ein Pagination-Schema hat (limit/offset/"
|
||||||
|
"next oder total > limit): ALLE Seiten in EINEM Tool-Call holen, "
|
||||||
|
"in EINEM Snapshot durchsuchen, ERST DANN handeln.\n"
|
||||||
|
"\n"
|
||||||
|
"Antipattern (31.05.2026, Stefan reproduziert mit 'Playlist Prodigy "
|
||||||
|
"raussuchen'):\n"
|
||||||
|
" - run_spotify path=/v1/me/playlists?limit=50\n"
|
||||||
|
" → 'nicht dabei'\n"
|
||||||
|
" - run_spotify path=/v1/me/playlists?limit=50&offset=50\n"
|
||||||
|
" → 'gefunden, ID=X' (46 Tracks)\n"
|
||||||
|
" - run_spotify path=/v1/me/player/play body={context_uri: ...:X}\n"
|
||||||
|
" → spielt aber FALSCHE Playlist\n"
|
||||||
|
" - Neue Suche, wieder paginiert → drittes Match ID=Y (15 Tracks)\n"
|
||||||
|
" - Insgesamt drei verschiedene IDs fuer dieselbe gesuchte Playlist\n"
|
||||||
|
" generiert, am Ende die falsche gespielt.\n"
|
||||||
|
"\n"
|
||||||
|
"Wurzel: Spotify sortiert /v1/me/playlists nach recently-played. "
|
||||||
|
"Zwischen aufeinanderfolgenden paginierten Calls AENDERT SICH die "
|
||||||
|
"Reihenfolge wenn parallel was abgespielt wird. Teilresultate aus "
|
||||||
|
"verschiedenen Calls vergleichen → inkonsistent.\n"
|
||||||
|
"\n"
|
||||||
|
"Richtig fuer Spotify (seit 31.05.2026 unterstuetzt):\n"
|
||||||
|
" run_spotify path=/v1/me/playlists?limit=50&_all=true\n"
|
||||||
|
" → Skill paginiert intern, liefert {items, total, fetched_count}.\n"
|
||||||
|
" → In items[] suchen, EINE ID waehlen, sofort handeln.\n"
|
||||||
|
" → Match-Logik: bevorzugt exakter Name (case-insensitive). "
|
||||||
|
"Wenn mehrere Substring-Matches: explizit nachfragen statt raten.\n"
|
||||||
|
"\n"
|
||||||
|
"Wann _all=true sinnvoll:\n"
|
||||||
|
" - /v1/me/playlists (alle eigenen Playlists)\n"
|
||||||
|
" - /v1/playlists/{id}/tracks (alle Tracks einer Playlist)\n"
|
||||||
|
" - /v1/me/tracks (Liked Songs)\n"
|
||||||
|
" - /v1/search?type=playlist&q=... (Such-Ergebnisse mit next)\n"
|
||||||
|
" - Andere Endpunkte mit items+next-Schema.\n"
|
||||||
|
"\n"
|
||||||
|
"Wann NICHT _all=true:\n"
|
||||||
|
" - /v1/me/player/currently-playing (kein Listen-Endpunkt)\n"
|
||||||
|
" - /v1/me/player/devices (kurze Liste, kein next)\n"
|
||||||
|
" - Wenn Du explizit nur 'die ersten 10' willst.\n"
|
||||||
|
"\n"
|
||||||
|
"Fuer andere Skills (yt-dlp, andere APIs) die noch kein _all "
|
||||||
|
"unterstuetzen: manuell paginieren bis total erreicht, ALLES in "
|
||||||
|
"EINEM mentalen Snapshot mergen, NIEMALS auf Teilresultaten "
|
||||||
|
"Entscheidungen treffen. Wenn zwei Pagination-Runs unterschiedliche "
|
||||||
|
"Matches liefern: ehrlich melden ('zwei verschiedene Playlists "
|
||||||
|
"namens X gefunden — welche meinst Du?') statt sich auf eine "
|
||||||
|
"festzulegen."
|
||||||
|
),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+21
-5
@@ -683,8 +683,13 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
|
|||||||
timed_out = True
|
timed_out = True
|
||||||
duration = time.time() - t0
|
duration = time.time() - t0
|
||||||
|
|
||||||
# Log schreiben (gekuerzt damit es nicht explodiert)
|
# Log auf der Disk wird gekuerzt (8000 chars) — sonst sammeln sich
|
||||||
record = {
|
# logs/*.json mit MBs an grossen Skill-Outputs an. Der Return-Value
|
||||||
|
# an den Caller (Agent) bekommt aber den vollen Output, dort wird
|
||||||
|
# nochmal in agent.py auf 50000 gecappt. Stefan-Fall: spotify-Skill
|
||||||
|
# mit _all=true liefert 50+ KB JSON, das hier wurde vorher auf 8 KB
|
||||||
|
# gekappt → ARIA sah immer nur den Anfang der Liste.
|
||||||
|
log_record = {
|
||||||
"ts": _now(),
|
"ts": _now(),
|
||||||
"args": args or {},
|
"args": args or {},
|
||||||
"exit_code": exit_code,
|
"exit_code": exit_code,
|
||||||
@@ -694,7 +699,7 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
|
|||||||
"timed_out": timed_out,
|
"timed_out": timed_out,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
log_path.write_text(json.dumps(record, indent=2, ensure_ascii=False), encoding="utf-8")
|
log_path.write_text(json.dumps(log_record, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -703,8 +708,19 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
|
|||||||
manifest["use_count"] = int(manifest.get("use_count", 0)) + 1
|
manifest["use_count"] = int(manifest.get("use_count", 0)) + 1
|
||||||
write_manifest(name, manifest)
|
write_manifest(name, manifest)
|
||||||
|
|
||||||
record["ok"] = exit_code == 0
|
# Return-Value: nicht kuerzen (Agent kuerzt downstream selbst). Nur
|
||||||
record["log_path"] = str(log_path)
|
# die Disk-Log-Variante war beschnitten.
|
||||||
|
record = {
|
||||||
|
"ts": log_record["ts"],
|
||||||
|
"args": log_record["args"],
|
||||||
|
"exit_code": exit_code,
|
||||||
|
"duration_sec": log_record["duration_sec"],
|
||||||
|
"stdout": out_text or "",
|
||||||
|
"stderr": err_text or "",
|
||||||
|
"timed_out": timed_out,
|
||||||
|
"ok": exit_code == 0,
|
||||||
|
"log_path": str(log_path),
|
||||||
|
}
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user