feat(skillmgr): 原生技能管理器插件 + OpenClaw 兼容层职责分离

新增 internal/plugins/skillmgr(native skill 全生命周期 owner):
- skill_list/info/load/unload/enable/disable/create/export/install
- skill_create 两步式:先生成骨架模板,LLM 补全后传 content 覆盖写入
  (plugin.ValidateSKILLContent 校验)并自动加载生效
- .skm 分发包(tar.gz):packSkill/unpackSkill 含 TarSlip 防护
  (拒绝绝对路径/../逃逸、强制单根目录、校验包内 SKILL.md)
- skills 目录扫描:纯 SKILL.md/skill.json 条目归本插件;
  sidecar(main.js/main.py)/OC plugin(openclaw.plugin.json) 留给兼容层

clawhubadapter 职责分离(OpenClaw 兼容层不再持有 native skill):
- 删除 p.skills 字段与 default 分支 LoadSKILL 逻辑
- 发现纯 SKILL 条目改为发布 events.EventSkillDetected 移交事件,
  由 skillmgr 订阅注册;启动时序 c<s 下全扫兜底,事件用于热新增
- claw_list/plugin_info 不再输出 SKILL 段,统一走 skill_list

方案B prompt 注入:
- agentCore 新增 SkillIndexProvider 接口 + SetSkillIndexProvider
- buildSystemPrompt 注入【可用技能】轻量索引(名称+版本+描述),
  LLM 匹配场景时主动 skill_info 拉全文按文档执行
- main.go 在插件加载后将 skillmgr 实例接线到 agent

内核小修:
- extractDescription 跳过 YAML frontmatter 块(此前所有带 frontmatter
  的 SKILL.md 描述都被误判为 '---')
- extractField 剥离 YAML 成对引号(version: "1.0" 不再带尾引号)
- plugin.ValidateSKILLContent 导出供生成侧校验
This commit is contained in:
JianFeeeee
2026-08-25 20:25:57 +08:00
parent a4e0cfcaa1
commit b10868f7cb
12 changed files with 1454 additions and 299 deletions

View File

@ -18,6 +18,7 @@ import (
"sync"
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
@ -46,7 +47,6 @@ type Plugin struct {
name string
skillsDir string
simulatorDir string
skills []*plugin.SKILLPlugin
sidecars []*sidecarProcess
manager *sidecarProcess
mu sync.Mutex
@ -163,13 +163,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
case hasOCManifest || hasOCPackage:
log.Printf("[clawhubadapter] ocplugin %s handled by manager", entry.Name())
default:
sk, err := plugin.LoadSKILL(skillPath)
if err != nil {
log.Printf("[clawhubadapter] load skill %s: %v", entry.Name(), err)
continue
}
p.skills = append(p.skills, sk)
log.Printf("[clawhubadapter] loaded skill: %s v%s", sk.Name(), sk.Version())
// 纯 SKILL 类型:不属于 OpenClaw 兼容层,移交原生 skillmgr 管理。
// skillmgr 启动时也会自行全扫(本插件启动更早的场景由事件补齐)。
p.publishSkillDetected(s, skillPath)
}
}
}
@ -592,16 +588,6 @@ func (p *Plugin) handlePluginUninstall(args map[string]interface{}) (interface{}
}
p.sidecars = aliveSidecars
// 2. Remove from SKILL list if present
var aliveSkills []*plugin.SKILLPlugin
for _, sk := range p.skills {
if sk.Name() != name {
aliveSkills = append(aliveSkills, sk)
} else {
logs = append(logs, fmt.Sprintf("已移除 SKILL 插件: %s v%s", sk.Name(), sk.Version()))
}
}
p.skills = aliveSkills
p.mu.Unlock()
// 3. Try manager for OC plugins
@ -690,12 +676,6 @@ func (p *Plugin) handlePluginList(args map[string]interface{}) (interface{}, err
}
}
}
if len(p.skills) > 0 {
parts = append(parts, fmt.Sprintf("\nSKILL 插件 (%d):", len(p.skills)))
for _, sk := range p.skills {
parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version()))
}
}
caps := p.dispatcher.Capabilities()
if len(caps) > 0 {
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps)))
@ -703,7 +683,7 @@ func (p *Plugin) handlePluginList(args map[string]interface{}) (interface{}, err
parts = append(parts, fmt.Sprintf(" %s", c))
}
}
if len(result.Plugins) == 0 && len(p.sidecars) <= 1 && len(p.skills) == 0 {
if len(result.Plugins) == 0 && len(p.sidecars) <= 1 {
parts = append(parts, "没有已安装的插件。")
}
p.mu.Unlock()
@ -738,13 +718,6 @@ func (p *Plugin) handlePluginList(args map[string]interface{}) (interface{}, err
}
}
if len(p.skills) > 0 {
parts = append(parts, fmt.Sprintf("\nSKILL 插件 (%d):", len(p.skills)))
for _, sk := range p.skills {
parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version()))
}
}
caps := p.dispatcher.Capabilities()
if len(caps) > 0 {
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps)))
@ -753,8 +726,8 @@ func (p *Plugin) handlePluginList(args map[string]interface{}) (interface{}, err
}
}
if len(p.sidecars) == 0 && len(p.skills) == 0 {
parts = append(parts, "没有已安装的插件。")
if len(p.sidecars) == 0 {
parts = append(parts, "没有已安装的插件(原生技能由 skillmgr 管理,用 skill_list 查看)。")
}
return map[string]interface{}{
@ -811,11 +784,6 @@ func (p *Plugin) handlePluginInfo(args map[string]interface{}) (interface{}, err
}
}
}
for _, sk := range p.skills {
if sk.Name() == name {
typ = "SKILL"
}
}
p.mu.Unlock()
}
@ -1264,12 +1232,23 @@ func (p *Plugin) Stop() error {
sp.Close()
}
p.sidecars = nil
p.skills = nil
p.manager = nil
p.sdk = nil
return nil
}
// publishSkillDetected 发布 skill_detected 事件,将纯 SKILL 条目移交原生
// skillmgr 插件注册。OpenClaw 兼容层不持有 native skill 状态。
func (p *Plugin) publishSkillDetected(s *sdk.PluginSDK, path string) {
s.Publish(&events.Event{
Type: events.EventSkillDetected,
Source: p.name,
Payload: map[string]interface{}{"path": path},
Timestamp: time.Now().Unix(),
})
log.Printf("[clawhubadapter] skill %s detected, handed off to skillmgr", filepath.Base(path))
}
func errorResult(msg string) interface{} {
return map[string]interface{}{
"isError": true,
@ -1338,13 +1317,8 @@ func (p *Plugin) reloadPlugin(name string) error {
case hasOCManifest || hasOCPackage:
return p.loadOCPlugin(p.sdk, pluginDir, name)
default:
sk, err := plugin.LoadSKILL(pluginDir)
if err != nil {
return fmt.Errorf("load skill: %w", err)
}
p.mu.Lock()
p.skills = append(p.skills, sk)
p.mu.Unlock()
// 纯 SKILL 类型:移交原生 skillmgr(与 Start 扫描行为一致)
p.publishSkillDetected(p.sdk, pluginDir)
return nil
}
}