Compare commits

..
Author SHA1 Message Date
duffyduck 517a8ea942 release(agent): bump to 0.0.1.5 2026-10-03 12:40:58 +02:00
duffyduckandClaude Opus 5 4589f4abc3 feat(android-agent): Einblendung geht weg, wenn Stefan etwas tut — statt nach fester Zeit
Eine feste Anzeigedauer ist immer geraten: mal zu kurz zum Lesen, mal steht sie
laengst im Weg. `seconds=0` liess die Einblendung bisher einfach stehen.

Jetzt heisst seconds=0: bleibt, bis der Nutzer etwas tut.

- Fernbedienung: der Accessibility-Dienst bekommt onKeyEvent und raeumt die
  Einblendung weg. Er gibt die Taste mit `return false` WEITER an den Film —
  es geht also kein Tastendruck verloren und der Player bleibt bedienbar.
  Dafuer brauchte der Dienst canRequestFilterKeyEvents + flagRequestFilterKeyEvents.
- Touch: FLAG_WATCH_OUTSIDE_TOUCH meldet eine Beruehrung ausserhalb. Die geht
  dank FLAG_NOT_TOUCHABLE gleichzeitig an die App darunter durch.
- Notbremse: nach 10 Minuten verschwindet sie in jedem Fall.

Bewusst NICHT der naheliegende Weg (Overlay faengt Tasten selbst ab): dafuer
muessten FLAG_NOT_FOCUSABLE und FLAG_NOT_TOUCHABLE weg, und dann nimmt die
Einblendung dem laufenden Film JEDE Eingabe, solange sie steht. Genau das soll
sie nicht.

host_notify beschreibt seconds=0 jetzt als den richtigen Wert fuer alles zum
Lesen (Wetter, Listen, Zahlen) und die feste Dauer fuer kurze Hinweise.

APK baut durch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 12:29:14 +02:00
duffyduckandClaude Opus 5 e67659b78a feat(diagnostic): warnt, wenn der Proxy mit veralteten Patches laeuft
Der Proxy kopiert proxy-patches/*.js beim START ueber den npm-Code. Wer nach
einem git pull nur `restart` macht — oder neu startet, BEVOR der Pull durch ist
— laeuft still mit der alten Fassung weiter. Nichts deutet darauf hin: der
Container ist "Up", die Oberflaeche sieht normal aus, die Aenderung wirkt
einfach nicht.

Das hat heute zweimal Zeit gekostet: einmal wirkte ein Proxy-Fix nicht, einmal
blieb die Modell-Liste alt, obwohl der Code laengst gepullt war (Proxy
gestartet 08:51, Patches gepullt 09:28).

Die Einstellungen vergleichen jetzt die Zeitstempel der Quelldateien mit den
eingespielten und zeigen einen Hinweis samt fertigem Befehl — inklusive der
Angabe, welche Datei wann geaendert und wann eingespielt wurde. Toleranz 5s,
weil Quelle und Kopie beim Start kurz nacheinander entstehen. Ein Fehler beim
Pruefen loest bewusst KEINEN Alarm aus, sonst stuende dort eine Warnung, die
nichts ueber die Patches aussagt.

Dazu: der Datenschutz-Block sagt jetzt ausdruecklich, dass KEIN Neustart noetig
ist — die CLI liest ~/.claude/settings.json bei jedem Aufruf neu. Nur ein
gerade laufender Turn nutzt noch die alte Einstellung.

4 Pruefungen gegen den echten Container, in beide Richtungen: schweigt nach
frischem Start, erkennt eine veraltete Kopie samt Zeitstempeln, schweigt nach
Wiederherstellung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 11:38:04 +02:00
8 changed files with 170 additions and 11 deletions
+12 -1
View File
@@ -1638,7 +1638,18 @@ META_TOOLS = [
"host": {"type": "string", "description": "Host-ID/Name (z.B. 'Fire-TV Schlafzimmer')."},
"text": {"type": "string", "description": "Der anzuzeigende Text."},
"title": {"type": "string", "description": "Ueberschrift (Default 'ARIA')."},
"seconds": {"type": "integer", "description": "Anzeigedauer (Default 12)."},
"seconds": {
"type": "integer",
"description": (
"Anzeigedauer in Sekunden (Default 12). "
"0 = stehen lassen, bis Stefan selbst wegdrueckt "
"(Fernbedienung: beliebige Taste, Handy: daneben "
"tippen). Nimm 0 fuer alles zum LESEN — Wetter, "
"Listen, Zahlen: eine feste Dauer ist da immer "
"geraten und meist zu kurz. Kurze Hinweise waehrend "
"eines Films ruhig mit Dauer."
),
},
"mode": {"type": "string", "description": "overlay | speech | both"},
"focus": {"type": "string", "description": "Bei Sprachausgabe: 'duck' (laufende Medien werden leiser, Standard) | 'pause' (Film/Musik anhalten) | 'none'."},
"image_path": {
+46 -2
View File
@@ -602,6 +602,24 @@
selbst (Skills + skill_create Meta-Tool). Es gibt keine granulare
Permission-Maske, Brain weiss zur Laufzeit welche Tools es hat. -->
<!-- Warnung: Proxy laeuft mit veralteten Patches.
Der Proxy kopiert proxy-patches/ beim START in den npm-Code. Wer nach
einem git pull nur neu startet statt force-recreate — oder vor dem
Pull neu startet — laeuft weiter mit der alten Fassung, ohne dass es
irgendwo auffaellt. Das hat schon zweimal Zeit gekostet: einmal wirkte
ein Fix nicht, einmal blieb die Modell-Liste alt. -->
<div id="proxy-patch-warning" style="display:none;margin-bottom:16px;padding:12px 14px;
background:#2A1A08;border:1px solid #FF9F0A;border-radius:8px;">
<div style="color:#FF9F0A;font-size:13px;font-weight:600;margin-bottom:6px;">
⚠ Der Proxy läuft mit veralteten Patches
</div>
<div id="proxy-patch-detail" style="color:#C8A878;font-size:11px;line-height:1.6;"></div>
<div style="margin-top:8px;">
<code style="background:#12121E;padding:4px 8px;border-radius:4px;color:#E0E0F0;font-size:11px;display:inline-block;">
docker compose up -d --force-recreate proxy</code>
</div>
</div>
<!-- Brain-Model-Einstellung -->
<div class="settings-section">
<h2>Sprachmodell (Brain)</h2>
@@ -659,8 +677,13 @@
<div class="card" style="max-width:620px;">
<div style="font-size:11px;color:#8888AA;margin-bottom:12px;line-height:1.55;">
Drei Kanäle, über die Claude Code <strong>zusätzlich</strong> zur eigentlichen
Anfrage Daten verschickt. Gesetzt wird in <code>~/.claude/settings.json</code> —
greift bei jeder neuen Anfrage, <strong>kein Neustart nötig</strong>.
Anfrage Daten verschickt.
<br><br>
<span style="color:#3FFF9F;">✓ Kein Neustart nötig.</span>
Gesetzt wird in <code>~/.claude/settings.json</code>, und die Claude-CLI liest
die Datei bei <em>jedem</em> Aufruf neu. Speichern genügt — die nächste Anfrage
nutzt die neue Einstellung. Nur ein <em>gerade laufender</em> Turn arbeitet noch
mit der alten, weil sein Prozess schon gestartet ist.
<br><br>
Davon unabhängig ist, ob Anthropic mit deinen Unterhaltungen Modelle
trainiert — das steht in deinem Konto unter
@@ -2748,6 +2771,26 @@
return;
}
if (msg.type === 'proxy_patch_state') {
const box = document.getElementById('proxy-patch-warning');
const det = document.getElementById('proxy-patch-detail');
if (box) {
const stale = msg.stale || [];
box.style.display = stale.length ? 'block' : 'none';
if (stale.length && det) {
const fmt = (ms) => new Date(ms).toLocaleString('de-DE',
{ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
det.innerHTML =
'Diese Dateien wurden nach dem letzten Proxy-Start geändert — der Proxy '
+ 'arbeitet noch mit der alten Fassung:<br>'
+ stale.map(s => '• <code>' + escapeHtml(s.file) + '</code> — geändert '
+ fmt(s.srcAt) + ', eingespielt ' + fmt(s.activeAt)).join('<br>')
+ '<br><br>Der Proxy kopiert die Patches nur beim <strong>Start</strong>. '
+ 'Ein <code>--build</code> reicht nicht:';
}
}
return;
}
if (msg.type === 'privacy_flags') { renderPrivacyFlags(msg); return; }
if (msg.type === 'privacy_flags_saved') {
const st = document.getElementById('privacy-flags-status');
@@ -4754,6 +4797,7 @@
refreshVoiceIdStatus();
send({ action: 'get_attachment_limit' });
send({ action: 'get_privacy_flags' });
send({ action: 'check_proxy_patches' });
} else if (tab === 'brain') {
loadBrainStatus();
loadBrainMemoryList();
+46
View File
@@ -3217,6 +3217,8 @@ wss.on("connection", (ws) => {
handleSetModel(ws, msg.model);
} else if (msg.action === "test_model") {
handleTestModel(ws, msg.model);
} else if (msg.action === "check_proxy_patches") {
handleCheckProxyPatches(ws);
} else if (msg.action === "get_privacy_flags") {
handleGetPrivacyFlags(ws);
} else if (msg.action === "set_privacy_flags") {
@@ -3647,6 +3649,50 @@ function handleSetModel(clientWs, model) {
return;
}
// ── Laufen im Proxy die aktuellen Patches? ──────────────────────────
//
// Der Proxy kopiert proxy-patches/*.js beim START ueber den npm-Code (siehe
// docker-compose.yml, command:). Wer nach einem git pull nur `restart` macht —
// oder neu startet, BEVOR der Pull durch ist — laeuft still mit der alten
// Fassung weiter. Das faellt sonst nirgends auf: der Container ist "Up", die
// Oberflaeche sieht normal aus, nur wirkt die Aenderung nicht.
//
// Darum: Zeitstempel der Quelldateien gegen die eingespielten vergleichen.
const PROXY_PATCH_MAP = [
["routes.js", "server/routes.js"],
["manager.js", "subprocess/manager.js"],
["openai-to-cli.js", "adapter/openai-to-cli.js"],
["cli-to-openai.js", "adapter/cli-to-openai.js"],
];
async function handleCheckProxyPatches(clientWs) {
try {
const dist = "/usr/local/lib/node_modules/claude-max-api-proxy/dist";
const pairs = PROXY_PATCH_MAP
.map(([src, dst]) => `/proxy-patches/${src} ${dist}/${dst}`).join(" ");
// %Y = mtime als Unix-Zeit, %n = Dateiname
const out = await dockerExec("aria-proxy", `stat -c '%Y %n' ${pairs} 2>/dev/null || true`);
const mtime = {};
for (const line of String(out || "").split("\n")) {
const m = line.trim().match(/^(\d+)\s+(.+)$/);
if (m) mtime[m[2]] = parseInt(m[1], 10);
}
const stale = [];
for (const [src, dst] of PROXY_PATCH_MAP) {
const a = mtime[`/proxy-patches/${src}`];
const b = mtime[`${dist}/${dst}`];
if (a == null || b == null) continue;
// Toleranz 5s: Quelle und Kopie entstehen beim Start kurz nacheinander.
if (a > b + 5) stale.push({ file: src, srcAt: a * 1000, activeAt: b * 1000 });
}
clientWs.send(JSON.stringify({ type: "proxy_patch_state", stale }));
} catch (err) {
// Kein Alarm bei einem Pruef-Fehler — sonst steht da eine Warnung, die
// gar nichts ueber die Patches aussagt.
clientWs.send(JSON.stringify({ type: "proxy_patch_state", stale: [], error: String((err && err.message) || err) }));
}
}
// ── Einstellungen: Datenschutz-Schalter von Claude Code ─────────────
//
// Drei Kanaele, ueber die Claude Code von sich aus Daten an Anthropic bzw. an
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId 'de.hackersoft.ariaagent'
minSdk 26
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
versionCode 104
versionName '0.0.1.4'
versionCode 105
versionName '0.0.1.5'
}
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
@@ -44,6 +44,29 @@ class AriaAccessibilityService : AccessibilityService() {
override fun onAccessibilityEvent(event: AccessibilityEvent?) { /* passiv */ }
override fun onInterrupt() { /* passiv */ }
/**
* Steht eine Einblendung offen, raeumt sie der naechste Tastendruck weg.
*
* Der Trick: wir geben IMMER `false` zurueck. Damit laeuft die Taste ganz
* normal weiter an den Film/die App — die Einblendung schluckt nichts. Sie
* muss dafuer auch nicht fokussierbar sein, kann also weiterhin ueber einem
* laufenden Video stehen, ohne die Fernbedienung zu blockieren.
*
* Deshalb dieser Weg und nicht "Overlay faengt Tasten selbst ab": ein
* fokussierbares Fenster wuerde der App darunter JEDE Eingabe nehmen,
* solange es steht.
*
* Eine feste Anzeigedauer ist naemlich immer falsch — mal zu kurz zum
* Lesen, mal steht sie ewig im Bild.
*/
override fun onKeyEvent(event: android.view.KeyEvent?): Boolean {
if (event != null && event.action == android.view.KeyEvent.ACTION_DOWN
&& OverlayView.isShowing()) {
OverlayView.hide(applicationContext)
}
return false // Taste NICHT verbrauchen
}
/** Strukturierter Baum des aktiven Fensters. Shape: {ok, result:{package,count,nodes[]}}. */
fun dump(): JSONObject {
val root = rootInActiveWindow
@@ -25,13 +25,33 @@ import android.widget.TextView
*
* Nicht fokussierbar (FLAG_NOT_FOCUSABLE) — die Fernbedienung steuert weiter die
* App darunter, die Einblendung schluckt keine Tastendruecke.
*
* WEGBEKOMMEN ohne feste Anzeigedauer (seconds = 0):
* - Fernbedienung: der Accessibility-Dienst sieht den naechsten Tastendruck und
* raeumt die Einblendung weg — er gibt die Taste trotzdem an den Film weiter
* (siehe AriaAccessibilityService.onKeyEvent).
* - Touch: FLAG_WATCH_OUTSIDE_TOUCH meldet uns eine Beruehrung AUSSERHALB der
* Einblendung. Die Beruehrung geht gleichzeitig an die App darunter — es
* geht also kein Tipp verloren.
* - Notbremse: nach HARD_CAP_SEC verschwindet sie in jedem Fall. Ohne das
* koennte eine vergessene Einblendung dauerhaft im Bild stehen.
*
* Eine feste Dauer ist naemlich immer falsch geraten — mal zu kurz zum Lesen,
* mal steht sie laengst im Weg.
*/
object OverlayView {
/** Auch eine "bleibt stehen"-Einblendung geht spaetestens danach weg. */
private const val HARD_CAP_SEC = 600
private val main = Handler(Looper.getMainLooper())
private var root: LinearLayout? = null
private var hideRunnable: Runnable? = null
/** Steht gerade eine Einblendung? Der Accessibility-Dienst fragt das, bevor
* er auf einen Tastendruck hin aufraeumt. */
fun isShowing(): Boolean = root != null
/** Hat der Nutzer "Ueber anderen Apps anzeigen" erlaubt? */
fun canDraw(ctx: Context): Boolean =
if (Build.VERSION.SDK_INT >= 23) Settings.canDrawOverlays(ctx) else true
@@ -98,6 +118,10 @@ object OverlayView {
type,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
// Meldet uns Beruehrungen AUSSERHALB, ohne sie zu
// schlucken — damit raeumt ein Tipp aufs Video die
// Einblendung weg und steuert trotzdem den Player.
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
android.graphics.PixelFormat.TRANSLUCENT,
).apply {
@@ -107,13 +131,23 @@ object OverlayView {
width = (ctx.resources.displayMetrics.widthPixels * 0.42f).toInt()
}
// Beruehrung ausserhalb -> wegraeumen. Die Beruehrung selbst
// geht dank FLAG_NOT_TOUCHABLE weiter an die App darunter.
box.setOnTouchListener { _, ev ->
if (ev.action == android.view.MotionEvent.ACTION_OUTSIDE) hideNow(ctx)
false
}
wm.addView(box, lp)
root = box
if (seconds > 0) {
hideRunnable = Runnable { hideNow(ctx) }
main.postDelayed(hideRunnable!!, seconds * 1000L)
}
// seconds > 0 : feste Dauer wie gehabt.
// seconds == 0: bleibt stehen, bis der Nutzer etwas tut
// (Taste via Accessibility-Dienst / Tipp daneben)
// — mit Notbremse, damit sie nie haengen bleibt.
val cap = if (seconds > 0) seconds else HARD_CAP_SEC
hideRunnable = Runnable { hideNow(ctx) }
main.postDelayed(hideRunnable!!, cap * 1000L)
} catch (_: Exception) {
root = null
}
@@ -2,8 +2,9 @@
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagRetrieveInteractiveWindows|flagReportViewIds"
android:accessibilityFlags="flagRetrieveInteractiveWindows|flagReportViewIds|flagRequestFilterKeyEvents"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true"
android:canRequestFilterKeyEvents="true"
android:notificationTimeout="100"
android:description="@string/accessibility_desc" />
+1 -1
View File
@@ -162,7 +162,7 @@ OUT_MAX_CHARS_HARD = int(os.environ.get("OUT_MAX_CHARS_HARD", "200000") or "2000
FILE_MAX_BYTES = int(os.environ.get("FILE_MAX_BYTES", str(10 * 1024 * 1024)) or str(10 * 1024 * 1024))
# Version (wird von release_agent.sh beim Release gesetzt).
AGENT_VERSION = "0.0.1.4"
AGENT_VERSION = "0.0.1.5"
HEARTBEAT_SEC = 25