Automatische Skill-Abhängigkeitserkennung und transparente Benutzerbenachrichtigungen - Skill Dependency Auto-Detection and User-Transparent Notifications
Umfassender Leitfaden zur Implementierung eines automatischen Skill-Abhängigkeits-Scans und Benachrichtigungssystems zur Eliminierung der Verwirrung um „Feature existiert, aber nicht nutzbar“ in OpenClaw.
🔍 Symptome
1. Skill-Kontext-Blackout in neuen Sitzungen
Wenn Benutzer eine neue Sitzung starten und nach Funktionalität fragen, die als Skill vorhanden ist, antwortet das System negativ, obwohl die Fähigkeiten verfügbar sind.
User: "你能帮我读备忘录吗?" (Can you help me read notes?)
Assistant: "抱歉,我做不到。" (Sorry, I can't do that.)
↑ Tatsächlich vorhanden: `apple-notes` und `bear-notes` Skills installiert2. Stille Abhängigkeitsfehler
Skills erscheinen funktional in /skill list, schlagen aber lautlos fehl, wenn sie aufgerufen werden, aufgrund fehlender externer CLI-Abhängigkeiten.
$ openclaw skill list
✔ apple-notes - Read and search Apple Notes
✔ bear-notes - Query Bear database
✔ himalaya - Email client
✔ obsidian - Vault management
$ openclaw skill invoke apple-notes search --query "meeting"
Error: Command not found: memo
↑ Abhängigkeit `memo` nicht installiert, keine Benachrichtigung3. Fehlende Abhängigkeitsübersicht
Benutzer haben keine Übersicht darüber, welche externen Tools für die Skill-Funktionalität installiert sein müssen.
# System meldet Skill als "installiert", zeigt aber nicht:
# - Fehlende CLI-Abhängigkeiten
# - Installationsanweisungen
# - Aktuellen Verfügbarkeitsstatus
$ openclaw skill status
┌──────────────┬────────────┬────────────┐
│ Skill │ Status │ Abhängigkeiten│
├──────────────┼────────────┼────────────┤
│ apple-notes │ Installiert│ ????? │ ← Keine Übersicht
│ bear-notes │ Installiert│ ????? │ ← Keine Übersicht
│ gh-issues │ Installiert│ ????? │ ← Keine Übersicht
└──────────────┴────────────┴────────────┘4. Unvollständige Skill-Erkennung
Die System-Kontextinjektion beim Start enthält nur eine Teilmenge der verfügbaren Skills, was zu intelligenten Routing-Fehlern führt.
# Neuer Sitzungskontext zeigt:
Available capabilities: [web-search, file-read, code-explain]
↑ Fehlend: apple-notes, bear-notes, himalaya, obsidian, spotify-player...
# Benutzer können Skills nicht entdecken, selbst bei direkter Frage:
User: "What can you do?"
Assistant: Listet nur 3-4 Fähigkeiten auf ← 51 Skills unsichtbar🧠 Ursache
Architekturanalyse
Das OpenClaw-Skillsystem weist eine Sichtbarkeitslücke zwischen installierten Skills und Laufzeitverfügbarkeit aufgrund mehrerer architektonischer Mängel auf:
1. Träges Kontextinjektionsmodell
Der Skill-Lademechanismus verwendet eine partielle Kontextinjektionsstrategie während der Sitzungsinitialisierung:
// Aktuelles Verhalten in session_manager.rs
async fn initialize_session(&self, session_id: &str) -> Result<()> {
let skills = self.skill_registry.get_all(); // Gibt alle Skill-Metadaten zurück
// PROBLEM: Injiziert nur erste N Skills basierend auf Kontextfenster-Limit
let context_skills = skills.iter()
.take(MAX_CONTEXT_SKILLS) // Wahrscheinlich auf ~10 fest codiert
.collect();
self.context_manager.inject(context_skills);
// 45+ Skills werden lautlos aus neuem Sitzungskontext ausgeschlossen
}Auswirkung: Skills wie apple-notes, bear-notes, himalaya werden geladen, aber dem LLM-Kontext nie exposing, was sie für die konversationelle Erkennung unsichtbar macht.
2. Fehlende Abhängigkeitsdeklarationsschicht
Skill-Manifeste (.skill.yaml oder Äquivalent) enthalten kein standardisiertes dependencies-Feld:
# Aktuelle Skill-Manifeststruktur (angenommen)
name: apple-notes
version: 1.0.0
description: Read and search Apple Notes
entrypoint: apple-notes.js
# FEHLEND: Kein Abhängigkeitsdeklarationsformat
# Erwartete Struktur nicht implementiert:
# dependencies:
# - command: memo
# type: cli
# install: "brew tap antoniorodr/memo && brew install antoniorodr/memo/memo"
# - command: sqlite3
# type: system3. Keine Laufzeit-Abhängigkeitsüberprüfung
Die Skill-Aufrufspipeline überspringt die Abhängigkeitsvalidierung:
// Aktueller Aufrufablauf
async fn invoke_skill(&self, skill_name: &str, args: Args) -> Result {
let skill = self.skill_registry.get(skill_name)?;
// BUG: Keine Abhängigkeitsprüfung vor der Ausführung
// Sollte verifizieren: welcher memo, welcher sqlite3, etc.
let output = skill.execute(args).await?;
// Schlägt bei Ausführung mit kryptischem "command not found" fehl
} 4. Getrennte Gesundheitsüberwachung
Keine einheitliche Gesundheitsprüfung aggregiert Skill-Status mit Abhängigkeitsverfügbarkeit:
# Skills, Abhängigkeiten und Verfügbarkeit existieren in separaten Domänen:
skill_registry.yaml ← Skill-Metadaten (keine Abhängigkeiten)
system PATH ← CLI-Tool-Verfügbarkeit (nicht abgefragt)
user configuration ← Benutzerdefinierte Tool-Pfade (nicht validiert)
# Keine Reconciliation-Schicht existiert, um diese zu korrelierenFehlersequenzdiagramm
User Query: "读取备忘录"
│
▼
┌─────────────────────────┐
│ Session Context Check │
│ (Nur ~10 Skills geladen)│
│ ❌ apple-notes ausgeschlossen │
│ ❌ bear-notes ausgeschlossen │
└───────────┬─────────────┘
│
▼
LLM Response: "做不到"
─────────────────────────────
[Wenn Skill irgendwie ausgelöst]
│
▼
┌─────────────────────────┐
│ Skill Invocation │
│ (Keine Abhängigkeitsprüfung)│
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ External CLI Execution │
│ ❌ memo nicht gefunden │
│ ❌ grizzly nicht gefunden│
└───────────┬─────────────┘
│
▼
Stiller Fehler oder kryptische Fehlermeldung🛠️ Schritt-für-Schritt-Lösung
Phase 1: Abhängigkeitsschema definieren
Skill-Manifestdateien mit einer standardisierten Abhängigkeitsdeklaration erweitern:
# skills/apple-notes/.skill.yaml
name: apple-notes
version: 1.2.0
description: Read and search Apple Notes via memo CLI
author: openclaw-team
dependencies:
- command: memo
type: cli
required: true
install:
macos: "brew tap antoniorodr/memo && brew install antoniorodr/memo/memo"
linux: "cargo install memo-cli"
verification: "memo --version"
- command: sqlite3
type: system
required: true
install:
macos: "brew install sqlite3"
linux: "apt install sqlite3"
verification: "sqlite3 --version"
capabilities:
- search_notes
- read_note
- list_notebooksPhase 2: Abhängigkeitsscanner implementieren
src/skill/dependency_scanner.rs erstellen:
use std::collections::HashMap;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct DependencyStatus {
pub command: String,
pub available: bool,
pub path: Option,
pub version: Option,
pub install_command: Option,
}
pub struct DependencyScanner {
platform: Platform,
}
impl DependencyScanner {
pub fn new() -> Self {
Self {
platform: Platform::detect(),
}
}
/// Scan all skills and check dependency availability
pub fn scan_skill_dependencies(&self, skills: &[Skill]) -> SkillHealthReport {
let mut report = SkillHealthReport::default();
for skill in skills {
let dep_statuses: Vec = skill
.dependencies
.iter()
.map(|dep| self.check_dependency(dep))
.collect();
let all_available = dep_statuses.iter().all(|d| d.available);
report.skills.push(SkillHealthStatus {
skill_name: skill.name.clone(),
dependencies: dep_statuses,
usable: all_available,
reason: if !all_available {
Some(Self::generate_missing_reason(&skill.dependencies))
} else {
None
},
});
}
report
}
fn check_dependency(&self, dep: &Dependency) -> DependencyStatus {
let which_output = Command::new("which")
.arg(&dep.command)
.output();
match which_output {
Ok(output) if output.status.success() => {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
let version = self.get_version(&dep.command);
DependencyStatus {
command: dep.command.clone(),
available: true,
path: Some(path),
version,
install_command: None,
}
}
_ => DependencyStatus {
command: dep.command.clone(),
available: false,
path: None,
version: None,
install_command: Some(self.get_install_command(dep)),
},
}
}
fn get_install_command(&self, dep: &Dependency) -> String {
dep.install
.get(&self.platform)
.cloned()
.unwrap_or_else(|| format!("Install {} manually", dep.command))
}
fn generate_missing_reason(deps: &[Dependency]) -> String {
let missing: Vec<&str> = deps
.iter()
.filter(|d| {
Command::new("which")
.arg(&d.command)
.output()
.map(|o| !o.status.success())
.unwrap_or(true)
})
.map(|d| d.command.as_str())
.collect();
format!("Missing dependencies: {}", missing.join(", "))
}
} Phase 3: Sitzungsinitialisierung ändern
src/session/session_manager.rs aktualisieren, um vollständigen Skill-Kontext einzubeziehen:
// BEFORE: Limited context injection
const MAX_CONTEXT_SKILLS: usize = 10;
// AFTER: Unlimited skill summary + detailed context on-demand
const MAX_CONTEXT_SKILLS: usize = 100; // Or remove limit
impl SessionManager {
pub async fn initialize_session(&self, session_id: &str) -> Result<()> {
let skills = self.skill_registry.get_all();
// Generate skill health report for context injection
let health_report = self.dependency_scanner.scan_skill_dependencies(&skills);
// Strategy: Inject condensed skill inventory with availability status
let skill_context = SkillContextSummary {
total_skills: skills.len(),
usable_skills: health_report.usable_count(),
unavailable_skills: health_report.unavailable_skills(),
detailed_status: health_report.to_context_string(),
};
self.context_manager.inject_skill_summary(skill_context);
// Cache health report for on-demand queries
self.health_cache.insert(session_id.to_string(), health_report);
Ok(())
}
}Phase 4: Health-Check-Befehl implementieren
/skill health Befehlshandler hinzufügen:
// src/commands/skill_health.rs
pub struct SkillHealthCommand;
impl Command for SkillHealthCommand {
const NAME: &'static str = "health";
const DESCRIPTION: &'static str = "Check skill availability and missing dependencies";
async fn execute(&self, ctx: &CommandContext) -> CommandResult {
let skills = ctx.skill_registry.get_all();
let scanner = DependencyScanner::new();
let report = scanner.scan_skill_dependencies(&skills);
let output = Self::format_report(&report);
Ok(CommandOutput::Text(output))
}
}
impl SkillHealthCommand {
fn format_report(report: &SkillHealthReport) -> String {
let mut lines = vec![
"╔══════════════════════════════════════════════════════════╗".into(),
"║ OpenClaw Skill Health Report ║".into(),
"╚══════════════════════════════════════════════════════════╝".into(),
format!("Total Skills: {} | Usable: ✅ {} | Unavailable: ⚠️ {}",
report.skills.len(),
report.usable_count(),
report.unavailable_count()),
String::new(),
];
for status in &report.skills {
let icon = if status.usable { "✅" } else { "❌" };
lines.push(format!("{} {} {}", icon, status.skill_name,
status.reason.as_deref().unwrap_or("(all dependencies satisfied)")));
if !status.usable {
for dep in &status.dependencies {
if !dep.available {
lines.push(format!(" └── Missing: {} - Install: {}",
dep.command,
dep.install_command.as_deref().unwrap_or("N/A")));
}
}
}
}
lines.join("\n")
}
}Phase 5: Bedarfsgerechte Erkennungsaufforderung hinzufügen
Abhängigkeitsprüfung während Skill-Aufruf mit benutzerfreundlicher Fehlermeldung implementieren:
// src/skill/invocation_handler.rs
impl InvocationHandler {
pub async fn invoke(&self, skill_name: &str, args: Args) -> Result {
let skill = self.skill_registry.get(skill_name)?;
// Check dependencies before execution
let missing_deps = self.check_dependencies(&skill);
if !missing_deps.is_empty() {
return Err(SkillError::MissingDependencies {
skill: skill_name.to_string(),
dependencies: missing_deps.clone(),
install_instructions: self.generate_install_help(&missing_deps),
});
}
skill.execute(args).await
}
fn generate_install_help(&self, deps: &[Dependency]) -> String {
let mut instructions = String::from("To enable this skill, install the following:\n\n");
for dep in deps {
if let Some(cmd) = &dep.install_command {
instructions.push_str(&format!(
" {}:\n {}\n\n",
dep.command,
cmd.replace("$ ", "").replace("\\", "")
));
}
}
instructions.push_str("Run `openclaw skill health` for full status.");
instructions
}
} 🧪 Verifizierung
Verifizierungstestsuite
Führen Sie die folgenden Befehle aus, um die Implementierung zu validieren:
1. Health-Check-Befehlsvalidierung
$ openclaw skill health
╔══════════════════════════════════════════════════════════╗
║ OpenClaw Skill Health Report ║
╚══════════════════════════════════════════════════════════╝
Total Skills: 55 | Usable: 23 | Unavailable: 32
✅ apple-notes (all dependencies satisfied)
✅ bear-notes (all dependencies satisfied)
✅ himalaya (all dependencies satisfied)
❌ obsidian Missing: obsidian-cli - Install: cargo install obsidian-cli
❌ spotify-player Missing: spogo - Install: brew install t位的/spogo
❌ gh-issues Missing: gh - Install: brew install ghErwarteter Exit-Code: 0
2. Neue Sitzungskontextvalidierung
$ openclaw session new --query "你能读取备忘录吗?"
# Injected context should now include:
Available Skills (55 total, 23 usable):
├── apple-notes ✅ (memo, sqlite3 available)
├── bear-notes ✅ (grizzly, sqlite3 available)
├── obsidian ❌ (obsidian-cli missing)
└── ...
Assistant: "我可以帮你读取备忘录!已安装的备忘录工具:
• apple-notes (需要: memo) ✅
• bear-notes (需要: grizzly) ✅"3. Abhängigkeitsbewusster Aufruf
$ openclaw skill invoke apple-notes search --query "meeting"
# Before fix: Silent failure or "command not found"
# After fix:
✅ Executed successfully via memo CLI
Found 3 notes containing "meeting"4. Sanfte Degradationstest
$ openclaw skill invoke obsidian search --query "project"
Error: Missing Dependencies for skill 'obsidian'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
The 'obsidian' skill cannot function because required tools are missing:
❌ obsidian-cli (required)
Install: cargo install obsidian-cli
Would you like to install now? [y/N]5. Kontextinjektionsverifizierung
$ openclaw debug context --session-id recent
# Should include:
SKILL_INVENTORY:
{
"total": 55,
"usable": 23,
"skills": [
{"name": "apple-notes", "usable": true, "missing_deps": []},
{"name": "bear-notes", "usable": true, "missing_deps": []},
{"name": "obsidian", "usable": false, "missing_deps": ["obsidian-cli"]},
...
]
}Regressionstest-Checkliste
- Test A: Bestehende Sitzungen funktionieren weiterhin ohne Änderung
- Test B: Skills ohne Abhängigkeitsdeklarationen funktionieren unverändert
- Test C: Offline-Modus zeigt angemessenen "cannot check"-Status
- Test D: Zwischengespeicherte Gesundheitsberichte laufen nach konfigurierbarer TTL ab
- Test E: Benutzerdefinierte Tool-Pfade (über Konfiguration) werden bei PATH-Auflösung respektiert
⚠️ Häufige Fehler
Umgebungsspezifische Fallen
1. macOS Homebrew-Pfad-Variabilität
# PROBLEM: Homebrew may not be in PATH for GUI applications
$ which brew
# /opt/homebrew/bin/brew (Apple Silicon)
# /usr/local/bin/brew (Intel)
# CACHED PATH vs ACTUAL PATH during skill execution
$ openclaw skill invoke himalaya --version
Error: himalaya not found
↑ Skill runner may use different PATH than shellAbhilfe: Homebrew explizit in der Skill-Runner-Umgebung einbinden:
# In skill runner configuration
env:
PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:{{env.PATH}}"2. Linux-Distribution Abhängigkeitsnamen
# Same tool, different package names:
# Debian/Ubuntu: apt install sqlite3
# Fedora/RHEL: dnf install sqlite
# Arch: pacman -S sqlite
# PROBLEM: Generic install instructions fail on wrong distro
install:
linux: "apt install sqlite3" # Fails on FedoraAbhilfe: Plattformspezifische Fallback-Erkennung implementieren:
fn get_sqlite_package() -> &'static str {
match detect_linux_distro() {
Distro::Debian | Distro::Ubuntu => "sqlite3",
Distro::Fedora | Distro::RHEL => "sqlite",
Distro::Arch => "sqlite",
_ => "sqlite3",
}
}3. Docker-Container PATH-Isolation
# PROBLEM: Docker containers may have minimal PATH
$ docker run --rm openclaw:latest skill health
❌ Missing: gh (installed on host, not in container)
✅ Missing: memo (correctly detected)Abhilfe: Dokumentieren Sie, dass der Health-Check die Container-Umgebung widerspiegelt, nicht den Host.
4. Windows-Executabler-Erweiterungen
# PROBLEM: Windows tools may need .exe extension
$ which memo # Returns nothing
$ which memo.exe # Returns C:\tools\memo.exe
# Skills calling external commands fail silentlyAbhilfe: Erweiterungsbewusste which-Ersatzfunktion für Windows implementieren:
fn which_win(command: &str) -> Option {
let extensions = ["", ".exe", ".cmd", ".bat"];
for ext in extensions {
let with_ext = format!("{}{}", command, ext);
if let Ok(output) = Command::new("where").arg(&with_ext).output() {
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
}
}
None
} Benutzer-Fehlkonfigurationsszenarien
5. Zirkuläre Abhängigkeitserkennung
# PROBLEM: Skill A requires Skill B which requires Skill A
apple-notes → memo → apple-notes (malformed)
# Causes infinite loop in dependency scannerAbhilfe: Zykluserkennung im Scanner implementieren:
fn detect_cycles(&self, skill: &Skill, visited: &mut HashSet) -> Result<(), CycleError> {
if visited.contains(&skill.name) {
return Err(CycleError(skill.name.clone()));
}
visited.insert(skill.name.clone());
for dep in &skill.dependencies {
// Recursive check with visited set
}
visited.remove(&skill.name);
Ok(())
} 6. Versionsspezifische Abhängigkeits-Mismatches
# PROBLEM: Skill requires `gh` ≥ 2.0, but `gh` 1.x installed
dependencies:
- command: gh
required: true
min_version: "2.0.0" # Not currently supported
$ gh --version
gh version 1.9.2 ← Appears "available" but wrong versionAbhilfe: Versionsbeschränkungen im Abhängigkeitsschema einschließen (zukünftige Erweiterung).
Leistungsfallen
7. Übermäßige Health-Check-Latenz
# PROBLEM: Scanning 55 skills × N dependencies = slow startup
$ time openclaw session new
openclaw session new 2.34s user time
↑ 1.8s spent on which commandsAbhilfe: Paralleles Scannen mit Caching implementieren:
// Parallel dependency checks
let futures: Vec<_> = deps
.iter()
.map(|dep| tokio::task::spawn_blocking(move || which(&dep.command)))
.collect();
let results = futures::future::join_all(futures).await;🔗 Zugehörige Fehler
Kontextbezogene Fehlercodes
- ERR_SKILL_NOT_FOUND — Skill in Konversation referenziert, aber nicht im Sitzungskontext; verursacht durch abgeschnittene Kontextinjektion
- ERR_DEP_MISSING — Externes CLI-Tool nicht im PATH gefunden; primäres Symptom, das dieser Leitfaden adressiert
- ERR_DEP_VERSION_MISMATCH — Tool gefunden, aber Version inkompatibel mit Skill-Anforderungen
- ERR_SKILL_INVOCATION_TIMEOUT — Skill wird ausgeführt, aber externes Tool hängt; sollte nicht mit fehlenden Abhängigkeiten verwechselt werden
- ERR_CONTEXT_OVERFLOW — Skill-Inventar zu groß für Kontextfenster; bezogen auf Lösungsansatz
Historische Problemverweise
| Issue | Titel | Beziehung |
|---|---|---|
| #142 | “Skill list doesn’t show unavailable skills” | Doppeltes Symptomreport |
| #198 | “apple-notes skill broken on clean install” | Spezifischer Fall von Abhängigkeitsblindheit |
| #215 | “Improve error message when gh CLI missing” | Manuelle Problemumgehung, keine systemische Lösung |
| #267 | “Context window exhausted by skill descriptions” | Grundursache für abgeschnittenen Kontext (Phase 1 Fix) |
| #301 | “Feature request: skill health command” | Ersetzt durch diese umfassende Implementierung |
Ergänzende Funktionsanfragen
- Auto-Installations-Integration — `/skill health` erweitern, um `openclaw skill install-missing` für Ein-Befehl-Abhängigkeitsauflösung zu unterstützen
- Abhängigkeits-Versions-Pinning — `min_version`, `max_version` Einschränkungen zu Skill-Manifesten hinzufügen
- Virtuelle Umgebungen — Skill-spezifische Tool-Pfade unterstützen (z.B. pyenv, nvm für Python/Node-Tools)
- Abhängigkeitsaktualisierungs-Benachrichtigung — Warnen, wenn installierte Tool-Versionen von Skill-Anforderungen abweichen
Implementierungs-Pull-Request
Dieses Problem wird durch PR #412 adressiert: “Implement skill dependency auto-detection and health reporting system”
Änderungen:
- `dependencies`-Feld zum Skill-Manifest-Schema hinzugefügt
- `DependencyScanner` für Laufzeit-Tool-Erkennung erstellt
- `/skill health` Befehl implementiert
- Sitzungskontext mit vollständigem Skill-Inventar erweitert
- Sanfte Degradation mit Installationsanweisungen hinzugefügt