skills api: auto check updates from clawhub on list

This commit is contained in:
DBT
2026-02-25 17:07:38 +00:00
parent ed60785fda
commit d0339d2754

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
@@ -512,19 +513,34 @@ func (s *RegistryServer) handleWebUISkills(w http.ResponseWriter, r *http.Reques
return return
} }
type skillItem struct { type skillItem struct {
Name string `json:"name"` Name string `json:"name"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
UpdateChecked bool `json:"update_checked"`
RemoteFound bool `json:"remote_found,omitempty"`
RemoteVersion string `json:"remote_version,omitempty"`
CheckError string `json:"check_error,omitempty"`
} }
items := make([]skillItem, 0, len(entries)) items := make([]skillItem, 0, len(entries))
checkUpdates := strings.TrimSpace(r.URL.Query().Get("check_updates")) != "0"
for _, e := range entries { for _, e := range entries {
if !e.IsDir() { if !e.IsDir() {
continue continue
} }
name := e.Name() name := e.Name()
enabled := !strings.HasSuffix(name, ".disabled") enabled := !strings.HasSuffix(name, ".disabled")
items = append(items, skillItem{Name: strings.TrimSuffix(name, ".disabled"), Enabled: enabled}) baseName := strings.TrimSuffix(name, ".disabled")
it := skillItem{Name: baseName, Enabled: enabled, UpdateChecked: checkUpdates}
if checkUpdates {
found, version, checkErr := queryClawHubSkillVersion(r.Context(), baseName)
it.RemoteFound = found
it.RemoteVersion = version
if checkErr != nil {
it.CheckError = checkErr.Error()
}
}
items = append(items, it)
} }
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "skills": items}) _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "skills": items, "source": "clawhub"})
case http.MethodPost: case http.MethodPost:
var body struct { var body struct {
Action string `json:"action"` Action string `json:"action"`
@@ -567,6 +583,71 @@ func (s *RegistryServer) handleWebUISkills(w http.ResponseWriter, r *http.Reques
} }
} }
func queryClawHubSkillVersion(ctx context.Context, skill string) (found bool, version string, err error) {
skill = strings.TrimSpace(skill)
if skill == "" {
return false, "", fmt.Errorf("skill empty")
}
cctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
cmd := exec.CommandContext(cctx, "clawhub", "search", skill, "--json")
out, runErr := cmd.Output()
if runErr != nil {
return false, "", runErr
}
var payload interface{}
if err := json.Unmarshal(out, &payload); err != nil {
return false, "", err
}
lowerSkill := strings.ToLower(skill)
var walk func(v interface{}) (bool, string)
walk = func(v interface{}) (bool, string) {
switch t := v.(type) {
case map[string]interface{}:
name := strings.ToLower(strings.TrimSpace(anyToString(t["name"])))
if name == "" {
name = strings.ToLower(strings.TrimSpace(anyToString(t["id"])))
}
if name == lowerSkill || strings.Contains(name, lowerSkill) {
ver := strings.TrimSpace(anyToString(t["version"]))
if ver == "" {
ver = strings.TrimSpace(anyToString(t["latest_version"]))
}
return true, ver
}
for _, vv := range t {
if ok, ver := walk(vv); ok {
return ok, ver
}
}
case []interface{}:
for _, vv := range t {
if ok, ver := walk(vv); ok {
return ok, ver
}
}
}
return false, ""
}
ok, ver := walk(payload)
return ok, ver, nil
}
func anyToString(v interface{}) string {
switch t := v.(type) {
case string:
return t
case fmt.Stringer:
return t.String()
default:
if v == nil {
return ""
}
b, _ := json.Marshal(v)
return string(b)
}
}
func (s *RegistryServer) handleWebUIExecApprovals(w http.ResponseWriter, r *http.Request) { func (s *RegistryServer) handleWebUIExecApprovals(w http.ResponseWriter, r *http.Request) {
if !s.checkAuth(r) { if !s.checkAuth(r) {
http.Error(w, "unauthorized", http.StatusUnauthorized) http.Error(w, "unauthorized", http.StatusUnauthorized)