feat(app+brain): App-Bugfixes + Skill-Mgmt-Tools + Voice-Speed persistent + Skill-Browser
App-Bugs:
- Trigger-Liste war leer: brainApi.listTriggers() cast'te {triggers: [...]}
direkt als Array, t.sort() warf — TriggerBrowser blieb leer. Fix: unwrap.
- GPS-Tracking startete erst bei SettingsScreen-Mount, nicht beim App-Boot.
Wenn Stefan direkt in den Chat ging, blieb GPS aus. Fix: restoreFromStorage()
in App.tsx useEffect.
- Text in Chat-Bubbles nicht markierbar / kein Copy-Mechanismus: Bubble jetzt
Pressable mit onLongPress + neues ⎘-Icon in Status-Row → openBubbleActions().
Alert-Menu mit "Ganzen Text teilen" + pro extrahierte URL/Mail/Tel eine
eigene Option. Share.share() — keine neuen Native-Deps noetig.
Brain — Skill-Mgmt:
- ARIA legte beim Skill-Umbau neue Versionen mit Suffix an (Skill-Friedhof),
weil sie kein Update/Delete-Tool kannte. Zwei neue META_TOOLS in agent.py:
skill_update (kann entry_code, readme, pip_packages, args, description,
active patchen — venv wird bei pip_packages-Aenderung rebuilt) + skill_delete.
- skills.py update_skill um entry_code/readme/pip_packages erweitert,
venv-Rebuild bei pip-Aenderung.
Bridge — Voice-Speed persistent:
- _next_speed_override war pro-Request-Override ohne Persistenz. Bei
Diagnostic-Chats / Trigger-Replies ohne vorherigen App-Chat fiel der Speed
auf 1.0 zurueck, ebenso nach Bridge-Restart. Jetzt: _persistent_xtts_speed
aus voice_config.json (xttsSpeed), wird nach jedem App-chat mit speed
autopersistiert. TTS-Generation faellt zurueck: per-Request > persistent > 1.0.
App — Feature 6:
- SkillBrowser.tsx: Liste aller Skills, Toggle aktiv/inaktiv, Detail-Modal
mit Args-Inputs, Ausfuehren mit Live-stdout/stderr, Logs der letzten 20
Runs, Loeschen. Settings-Sektion "Skills" (🛠️) zwischen Trigger und
Protokoll. brainApi.listSkills/getSkill/runSkill/updateSkill/deleteSkill/
getSkillLogs ergaenzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,67 @@ META_TOOLS = [
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "skill_update",
|
||||
"description": (
|
||||
"Aktualisiere einen EXISTIERENDEN Skill statt eine zweite Version "
|
||||
"mit `-v2`/`-new`/`-fixed` Suffix anzulegen. Stefan hasst Skill-"
|
||||
"Friedhoefe. Wenn Du `youtube2mp3` umbauen sollst → `skill_update` "
|
||||
"auf den bestehenden, NICHT `skill_create` mit neuem Namen.\n\n"
|
||||
"Du kannst gleichzeitig `entry_code` (Python-Code austauschen), "
|
||||
"`readme`, `pip_packages` (bei Aenderung wird die venv automatisch "
|
||||
"neu aufgebaut), `args`, `description` und `active` setzen. Felder "
|
||||
"die Du weglaesst bleiben unberuehrt.\n\n"
|
||||
"WENN Du Dir bei einem grundlegenden API-Bruch unsicher bist ob "
|
||||
"der Skill noch zum Namen passt: lieber `skill_delete` + "
|
||||
"`skill_create` mit neuem semantischen Namen statt eines "
|
||||
"halbgaren Updates."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Bestehender Skill-Name"},
|
||||
"entry_code": {"type": "string", "description": "Neuer Python-Code (optional)"},
|
||||
"readme": {"type": "string", "description": "Neuer README-Inhalt (optional)"},
|
||||
"pip_packages": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Neue pip-Pakete (ueberschreibt komplette Liste; triggert venv-Rebuild)",
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "Neues Args-Schema (optional)",
|
||||
},
|
||||
"description": {"type": "string", "description": "Neue Beschreibung (optional)"},
|
||||
"active": {"type": "boolean", "description": "Aktivieren/deaktivieren (optional)"},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "skill_delete",
|
||||
"description": (
|
||||
"Loescht einen Skill samt venv und Logs. Nutze das wenn:\n"
|
||||
"1. Stefan explizit sagt der Skill soll weg\n"
|
||||
"2. Du eine alte Skill-Version losgeworden bist nachdem `skill_create` "
|
||||
"mit besserem Namen erfolgreich war (Aufraeumen statt Skill-Friedhof)\n"
|
||||
"3. Ein Skill grundlegend kaputt und ein Update sich nicht mehr lohnt — "
|
||||
"in dem Fall bestaetige vorher kurz bei Stefan.\n\n"
|
||||
"Nicht rueckholbar."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
@@ -746,6 +807,46 @@ class Agent:
|
||||
f"- {s['name']} ({s['execution']}) {'aktiv' if s.get('active', True) else 'DEAKTIVIERT'}: {s.get('description', '')}"
|
||||
for s in items
|
||||
)
|
||||
if name == "skill_update":
|
||||
skill_name = (arguments.get("name") or "").strip()
|
||||
if not skill_name:
|
||||
return "FEHLER: name ist Pflicht."
|
||||
patch: dict = {}
|
||||
for k in ("entry_code", "readme", "description", "args", "active"):
|
||||
if k in arguments and arguments[k] is not None:
|
||||
patch[k] = arguments[k]
|
||||
if "pip_packages" in arguments and isinstance(arguments["pip_packages"], list):
|
||||
patch["pip_packages"] = arguments["pip_packages"]
|
||||
if not patch:
|
||||
return "FEHLER: keine Felder zum Update angegeben."
|
||||
try:
|
||||
manifest = skills_mod.update_skill(skill_name, patch)
|
||||
except ValueError as exc:
|
||||
return f"FEHLER: {exc}"
|
||||
# Side-Channel-Event als skill_created getarnt — gleiche Bubble-Mechanik
|
||||
# in App/Diagnostic; das Update soll fuer Stefan ebenfalls sichtbar werden.
|
||||
self._pending_events.append({
|
||||
"type": "skill_created",
|
||||
"skill": {
|
||||
"name": manifest["name"],
|
||||
"description": manifest.get("description", ""),
|
||||
"execution": manifest.get("execution", ""),
|
||||
"active": manifest.get("active", True),
|
||||
"setup_error": manifest.get("setup_error"),
|
||||
"updated": True,
|
||||
},
|
||||
})
|
||||
changed = ", ".join(sorted(patch.keys()))
|
||||
return f"OK — Skill '{skill_name}' aktualisiert ({changed}). active={manifest['active']}"
|
||||
if name == "skill_delete":
|
||||
skill_name = (arguments.get("name") or "").strip()
|
||||
if not skill_name:
|
||||
return "FEHLER: name ist Pflicht."
|
||||
try:
|
||||
skills_mod.delete_skill(skill_name)
|
||||
except ValueError as exc:
|
||||
return f"FEHLER: {exc}"
|
||||
return f"OK — Skill '{skill_name}' geloescht."
|
||||
if name.startswith("run_"):
|
||||
skill_name = name[len("run_"):]
|
||||
res = skills_mod.run_skill(skill_name, args=arguments)
|
||||
|
||||
@@ -194,14 +194,59 @@ def _setup_venv(skill_dir: Path, pip_packages: list[str]) -> None:
|
||||
|
||||
|
||||
def update_skill(name: str, patch: dict) -> dict:
|
||||
"""Aktualisiert einen bestehenden Skill. Manifest-Felder ueber den
|
||||
`allowed`-Filter, Code-Aenderungen ueber dedizierte Keys:
|
||||
|
||||
- `entry_code` (str) → ueberschreibt run.py / run.sh
|
||||
- `readme` (str) → ueberschreibt README.md
|
||||
- `pip_packages` (list) → ueberschreibt requirements.txt + venv-Rebuild
|
||||
(nur bei local-venv)
|
||||
"""
|
||||
manifest = read_manifest(name)
|
||||
if manifest is None:
|
||||
raise ValueError(f"Skill '{name}' nicht gefunden")
|
||||
d = _skill_dir(name)
|
||||
allowed = {"description", "args", "requires", "active", "version", "entry"}
|
||||
for k, v in patch.items():
|
||||
if k in allowed:
|
||||
manifest[k] = v
|
||||
|
||||
# Code austauschen
|
||||
if "entry_code" in patch and patch["entry_code"]:
|
||||
execution = manifest.get("execution", "local-venv")
|
||||
if execution == "local-venv":
|
||||
entry_path = d / "run.py"
|
||||
entry_path.write_text(patch["entry_code"], encoding="utf-8")
|
||||
else:
|
||||
entry_path = d / "run.sh"
|
||||
content = patch["entry_code"] if patch["entry_code"].startswith("#!") else "#!/usr/bin/env bash\nset -euo pipefail\n" + patch["entry_code"]
|
||||
entry_path.write_text(content, encoding="utf-8")
|
||||
entry_path.chmod(0o755)
|
||||
|
||||
# README austauschen
|
||||
if "readme" in patch and patch["readme"] is not None:
|
||||
(d / "README.md").write_text(patch["readme"], encoding="utf-8")
|
||||
|
||||
# pip_packages geaendert → requirements.txt + venv neu aufbauen
|
||||
if "pip_packages" in patch and manifest.get("execution") == "local-venv":
|
||||
pip_packages = patch["pip_packages"] or []
|
||||
(d / "requirements.txt").write_text("\n".join(pip_packages) + "\n", encoding="utf-8")
|
||||
# venv loeschen + neu aufbauen, damit alte Pakete weg sind
|
||||
venv = d / "venv"
|
||||
if venv.exists():
|
||||
shutil.rmtree(venv, ignore_errors=True)
|
||||
try:
|
||||
_setup_venv(d, pip_packages)
|
||||
# Falls vorher wegen Setup-Error deaktiviert war: jetzt frei
|
||||
manifest.pop("setup_error", None)
|
||||
manifest["active"] = patch.get("active", True)
|
||||
except Exception as exc:
|
||||
manifest["active"] = False
|
||||
manifest["setup_error"] = str(exc)[:500]
|
||||
logger.warning("Skill %s: venv-Rebuild fehlgeschlagen: %s", name, exc)
|
||||
|
||||
write_manifest(name, manifest)
|
||||
logger.info("Skill aktualisiert: %s (keys=%s)", name, sorted(patch.keys()))
|
||||
return manifest
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user