• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smallnest / goclaw / 21978860214

13 Feb 2026 07:45AM UTC coverage: 5.772% (+0.008%) from 5.764%
21978860214

push

github

chaoyuepan
improve web fetch

4 of 24 new or added lines in 10 files covered. (16.67%)

221 existing lines in 4 files now uncovered.

1517 of 26284 relevant lines covered (5.77%)

0.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/agent/skills.go
1
package agent
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "os"
8
        "os/exec"
9
        "path/filepath"
10
        "runtime"
11
        "sort"
12
        "strings"
13
        "time"
14

15
        "github.com/smallnest/goclaw/internal/logger"
16
        "github.com/smallnest/goclaw/skills"
17
        "go.uber.org/zap"
18
        "gopkg.in/yaml.v3"
19
)
20

21
// Skill 技能定义
22
type Skill struct {
23
        Name        string `yaml:"name"`
24
        Description string `yaml:"description"`
25
        Version     string `yaml:"version"`
26
        Author      string `yaml:"author"`
27
        Homepage    string `yaml:"homepage"`
28
        Always      bool   `yaml:"always"`
29
        Metadata    struct {
30
                OpenClaw struct {
31
                        Emoji    string `yaml:"emoji"`
32
                        Always   bool   `yaml:"always"`
33
                        Requires struct {
34
                                Bins       []string `yaml:"bins"`
35
                                AnyBins    []string `yaml:"anyBins"`
36
                                Env        []string `yaml:"env"`
37
                                Config     []string `yaml:"config"`
38
                                OS         []string `yaml:"os"`
39
                                PythonPkgs []string `yaml:"pythonPkgs"` // Python包依赖
40
                                NodePkgs   []string `yaml:"nodePkgs"`   // Node.js包依赖
41
                        } `yaml:"requires"`
42
                        Install []SkillInstall `yaml:"install"`
43
                } `yaml:"openclaw"`
44
        } `yaml:"metadata"`
45
        Requires SkillRequirements `yaml:"requires"` // 兼容旧格式
46
        Content  string            `yaml:"-"`        // 技能内容(Markdown)
47
        // 缺失的依赖信息
48
        MissingDeps *MissingDeps `yaml:"-"` // 解析时填充
49
}
50

51
// MissingDeps 缺失的依赖信息
52
type MissingDeps struct {
53
        Bins       []string `yaml:"bins"`       // 缺失的二进制
54
        AnyBins    []string `yaml:"anyBins"`    // 缺失的可选二进制
55
        Env        []string `yaml:"env"`        // 缺失的环境变量
56
        PythonPkgs []string `yaml:"pythonPkgs"` // 缺失的Python包
57
        NodePkgs   []string `yaml:"nodePkgs"`   // 缺失的Node.js包
58
}
59

60
// SkillRequirements 技能需求 (旧格式)
61
type SkillRequirements struct {
62
        Bins []string `yaml:"bins"`
63
        Env  []string `yaml:"env"`
64
}
65

66
// SkillInstall 技能安装配置
67
type SkillInstall struct {
68
        ID      string   `yaml:"id"`      // 安装方式唯一标识
69
        Kind    string   `yaml:"kind"`    // 安装方式: brew, apt, npm, pip, uv, go
70
        Formula string   `yaml:"formula"` // 包名 (brew, apt)
71
        Package string   `yaml:"package"` // 包名 (npm, pip, go)
72
        Bins    []string `yaml:"bins"`    // 安装后提供的可执行文件
73
        Label   string   `yaml:"label"`   // 安装说明
74
        OS      []string `yaml:"os"`      // 适用的操作系统
75
        Command string   `yaml:"command"` // 自定义安装命令
76
}
77

78
// SkillsLoader 技能加载器
79
type SkillsLoader struct {
80
        workspace    string
81
        skillsDirs   []string
82
        skills       map[string]*Skill
83
        alwaysSkills []string
84
        autoInstall  bool // 是否启用自动安装依赖
85
}
86

87
// NewSkillsLoader 创建技能加载器
88
func NewSkillsLoader(workspace string, skillsDirs []string) *SkillsLoader {
×
89
        return &SkillsLoader{
×
90
                workspace:   workspace,
×
91
                skillsDirs:  skillsDirs,
×
92
                skills:      make(map[string]*Skill),
×
93
                autoInstall: os.Getenv("GOCLAW_SKILL_AUTO_INSTALL") == "true",
×
94
        }
×
95
}
×
96

97
// SetAutoInstall 设置是否启用自动安装
98
func (l *SkillsLoader) SetAutoInstall(enabled bool) {
×
99
        l.autoInstall = enabled
×
100
}
×
101

102
// Discover 发现技能
103
func (l *SkillsLoader) Discover() error {
×
104
        // 只使用配置的技能目录(~/.goclaw/skills)
×
105
        for _, dir := range l.skillsDirs {
×
106
                if err := l.discoverInDir(dir); err != nil {
×
107
                        // 目录不存在是正常的,继续
×
108
                        if !os.IsNotExist(err) {
×
109
                                return err
×
110
                        }
×
111
                }
112
        }
113

114
        return nil
×
115
}
116

117
// discoverInDir 在目录中发现技能
118
func (l *SkillsLoader) discoverInDir(dir string) error {
×
119
        entries, err := os.ReadDir(dir)
×
120
        if err != nil {
×
121
                return err
×
122
        }
×
123

124
        for _, entry := range entries {
×
125
                if !entry.IsDir() {
×
126
                        // 跳过非目录文件
×
127
                        continue
×
128
                }
129

130
                skillPath := filepath.Join(dir, entry.Name())
×
131
                if err := l.loadSkill(skillPath); err != nil {
×
132
                        // 跳过无法加载的技能
×
133
                        continue
×
134
                }
135
        }
136

137
        return nil
×
138
}
139

140
// loadSkill 加载技能
141
func (l *SkillsLoader) loadSkill(path string) error {
×
142
        // 查找 SKILL.md 或 skill.md
×
143
        skillFile := filepath.Join(path, "SKILL.md")
×
144
        if _, err := os.Stat(skillFile); os.IsNotExist(err) {
×
145
                skillFile = filepath.Join(path, "skill.md")
×
146
                if _, err := os.Stat(skillFile); os.IsNotExist(err) {
×
147
                        return nil // 没有技能文件
×
148
                }
×
149
        }
150

151
        // 读取文件
152
        content, err := os.ReadFile(skillFile)
×
153
        if err != nil {
×
154
                return err
×
155
        }
×
156

157
        // 解析 YAML front matter(使用新解析器)
158
        var skill Skill
×
159
        if err := l.parseSkillMetadata(string(content), &skill); err != nil {
×
160
                return err
×
161
        }
×
162

163
        // 检查是否存在阻塞式需求(如 OS 不匹配),这类需求会导致跳过技能
164
        if !l.checkBlockingRequirements(&skill) {
×
165
                // 存在阻塞式需求,跳过该技能
×
166
                return nil
×
167
        }
×
168

169
        // 如果新解析器已经提取了内容,使用它;否则回退到旧方法
170
        if skill.Content == "" {
×
171
                skill.Content = l.extractContent(string(content))
×
172
        }
×
173

174
        // 计算缺失的依赖(用于显示给LLM)
175
        skill.MissingDeps = l.getMissingDeps(&skill)
×
176

×
177
        // 使用目录名作为技能名
×
178
        if skill.Name == "" {
×
179
                skill.Name = filepath.Base(path)
×
180
        }
×
181

182
        l.skills[skill.Name] = &skill
×
183

×
184
        // 记录 always 技能
×
185
        if skill.Always {
×
186
                l.alwaysSkills = append(l.alwaysSkills, skill.Name)
×
187
        }
×
188

189
        return nil
×
190
}
191

192
// checkBlockingRequirements 检查阻塞性需求(如 OS 不匹配)
193
// 返回 false 表示技能因阻塞性需求无法使用,应跳过加载
194
func (l *SkillsLoader) checkBlockingRequirements(skill *Skill) bool {
×
195
        // always 技能总是加载
×
196
        if skill.Always || skill.Metadata.OpenClaw.Always {
×
197
                return true
×
198
        }
×
199

200
        // 检查操作系统兼容性(阻塞性)
201
        if len(skill.Metadata.OpenClaw.Requires.OS) > 0 {
×
202
                currentOS := runtime.GOOS
×
203
                compatible := false
×
204
                for _, osName := range skill.Metadata.OpenClaw.Requires.OS {
×
205
                        if osName == currentOS {
×
206
                                compatible = true
×
207
                                break
×
208
                        }
209
                }
210
                if !compatible {
×
211
                        return false
×
212
                }
×
213
        }
214

215
        return true
×
216
}
217

218
// parseSkillMetadata 解析技能元数据(支持新旧格式)
219
func (l *SkillsLoader) parseSkillMetadata(content string, skill *Skill) error {
×
220
        // 首先尝试使用新的 frontmatter 解析器
×
221
        frontmatter := skills.ParseFrontmatter(content)
×
NEW
222
        if len(frontmatter) > 0 {
×
223
                // 从 frontmatter 中解析基本字段
×
224
                if name := frontmatter["name"]; name != "" {
×
225
                        skill.Name = name
×
226
                }
×
227
                if desc := frontmatter["description"]; desc != "" {
×
228
                        skill.Description = desc
×
229
                }
×
230
                if homepage := frontmatter["homepage"]; homepage != "" {
×
231
                        skill.Homepage = homepage
×
232
                }
×
233
                if always := frontmatter["always"]; always != "" {
×
234
                        skill.Always = always == "true"
×
235
                }
×
236

237
                // 解析 OpenClaw/goclaw 元数据
238
                metadata := skills.ParseOpenClawMetadata(frontmatter)
×
239
                if metadata != nil {
×
240
                        // 映射到旧的 Skill 结构
×
241
                        skill.Metadata.OpenClaw.Emoji = metadata.Emoji
×
242
                        skill.Metadata.OpenClaw.Always = metadata.Always
×
243
                        if metadata.Requires != nil {
×
244
                                skill.Metadata.OpenClaw.Requires.Bins = metadata.Requires.Bins
×
245
                                skill.Metadata.OpenClaw.Requires.AnyBins = metadata.Requires.AnyBins
×
246
                                skill.Metadata.OpenClaw.Requires.Env = metadata.Requires.Env
×
247
                                skill.Metadata.OpenClaw.Requires.Config = metadata.Requires.Config
×
248
                                skill.Metadata.OpenClaw.Requires.OS = metadata.Requires.OS
×
249
                        }
×
250

251
                        // 映射安装配置
252
                        for _, install := range metadata.Install {
×
253
                                skillInstall := SkillInstall{
×
254
                                        ID:      install.ID,
×
255
                                        Kind:    install.Kind,
×
256
                                        Label:   install.Label,
×
257
                                        Bins:    install.Bins,
×
258
                                        OS:      install.OS,
×
259
                                        Formula: install.Formula,
×
260
                                        Package: install.Package,
×
261
                                }
×
262
                                skill.Metadata.OpenClaw.Install = append(skill.Metadata.OpenClaw.Install, skillInstall)
×
263
                        }
×
264
                }
265

266
                // 提取内容(移除 frontmatter)
267
                skill.Content = skills.StripFrontmatter(content)
×
268
                return nil
×
269
        }
270

271
        // 回退到旧的 YAML 解析方式
272
        if !strings.HasPrefix(content, "---") {
×
273
                return nil // 没有 YAML front matter
×
274
        }
×
275

276
        endIndex := strings.Index(content[3:], "---")
×
277
        if endIndex == -1 {
×
278
                return nil // 没有结束分隔符
×
279
        }
×
280

281
        yamlContent := content[4 : endIndex+3]
×
282

×
283
        // 解析 YAML
×
284
        if err := yaml.Unmarshal([]byte(yamlContent), skill); err != nil {
×
285
                return err
×
286
        }
×
287

288
        return nil
×
289
}
290

291
// extractContent 提取内容(移除 YAML front matter)
292
func (l *SkillsLoader) extractContent(content string) string {
×
293
        if !strings.HasPrefix(content, "---") {
×
294
                return content
×
295
        }
×
296

297
        endIndex := strings.Index(content[3:], "---")
×
298
        if endIndex == -1 {
×
299
                return content
×
300
        }
×
301

302
        return content[endIndex+7:] // 跳过 "---\n"
×
303
}
304

305
// List 列出所有技能
306
func (l *SkillsLoader) List() []*Skill {
×
307
        result := make([]*Skill, 0, len(l.skills))
×
308
        for _, skill := range l.skills {
×
309
                result = append(result, skill)
×
310
        }
×
311
        return result
×
312
}
313

314
// Get 获取技能
315
func (l *SkillsLoader) Get(name string) (*Skill, bool) {
×
316
        skill, ok := l.skills[name]
×
317
        return skill, ok
×
318
}
×
319

320
// GetAlwaysSkills 获取始终加载的技能
321
func (l *SkillsLoader) GetAlwaysSkills() []string {
×
322
        return l.alwaysSkills
×
323
}
×
324

325
// BuildSummary 构建技能摘要
326
func (l *SkillsLoader) BuildSummary() string {
×
327
        if len(l.skills) == 0 {
×
328
                return "No skills available."
×
329
        }
×
330

331
        var summary string
×
332
        summary += fmt.Sprintf("# Available Skills (%d)\n\n", len(l.skills))
×
333

×
334
        for name, skill := range l.skills {
×
335
                summary += fmt.Sprintf("## %s\n", name)
×
336
                if skill.Description != "" {
×
337
                        summary += fmt.Sprintf("%s\n", skill.Description)
×
338
                }
×
339
                if skill.Author != "" {
×
340
                        summary += fmt.Sprintf("Author: %s\n", skill.Author)
×
341
                }
×
342
                if skill.Version != "" {
×
343
                        summary += fmt.Sprintf("Version: %s\n", skill.Version)
×
344
                }
×
345
                summary += "\n"
×
346
        }
347

348
        return summary
×
349
}
350

351
// LoadContent 加载技能内容
352
func (l *SkillsLoader) LoadContent(name string) (string, error) {
×
353
        skill, ok := l.skills[name]
×
354
        if !ok {
×
355
                return "", fmt.Errorf("skill not found: %s", name)
×
356
        }
×
357

358
        return skill.Content, nil
×
359
}
360

361
// InstallDependencies 安装技能依赖
362
func (l *SkillsLoader) InstallDependencies(skillName string) error {
×
363
        skill, ok := l.skills[skillName]
×
364
        if !ok {
×
365
                return fmt.Errorf("skill not found: %s", skillName)
×
366
        }
×
367

368
        // 检查二进制依赖并安装
369
        for _, bin := range skill.Metadata.OpenClaw.Requires.Bins {
×
370
                if _, err := exec.LookPath(bin); err != nil {
×
371
                        if err := l.tryInstallBinary(skill, bin); err != nil {
×
372
                                return fmt.Errorf("failed to install %s for skill %s: %w", bin, skillName, err)
×
373
                        }
×
374
                }
375
        }
376

377
        for _, bin := range skill.Metadata.OpenClaw.Requires.AnyBins {
×
378
                if _, err := exec.LookPath(bin); err == nil {
×
379
                        // 有一个已经安装了,跳过
×
380
                        break
×
381
                }
382
                if err := l.tryInstallBinary(skill, bin); err != nil {
×
383
                        logger.Warn("Failed to install optional dependency",
×
384
                                zap.String("skill", skillName),
×
385
                                zap.String("bin", bin),
×
386
                                zap.Error(err))
×
387
                }
×
388
        }
389

390
        // 检查Python包依赖并安装
391
        for _, pkg := range skill.Metadata.OpenClaw.Requires.PythonPkgs {
×
392
                if err := l.checkPythonPackage(pkg); err != nil {
×
393
                        if err := l.tryInstallPythonPackage(pkg); err != nil {
×
394
                                return fmt.Errorf("failed to install Python package %s for skill %s: %w", pkg, skillName, err)
×
395
                        }
×
396
                }
397
        }
398

399
        // 检查Node.js包依赖并安装
400
        for _, pkg := range skill.Metadata.OpenClaw.Requires.NodePkgs {
×
401
                if err := l.checkNodePackage(pkg); err != nil {
×
402
                        if err := l.tryInstallNodePackage(pkg); err != nil {
×
403
                                return fmt.Errorf("failed to install Node.js package %s for skill %s: %w", pkg, skillName, err)
×
404
                        }
×
405
                }
406
        }
407

408
        return nil
×
409
}
410

411
// tryInstallBinary 尝试安装二进制文件
412
func (l *SkillsLoader) tryInstallBinary(skill *Skill, bin string) error {
×
413
        installConfig := l.findInstallConfig(skill, bin)
×
414
        if installConfig == nil {
×
415
                return fmt.Errorf("no install config for %s", bin)
×
416
        }
×
417

418
        // 检查操作系统是否匹配
419
        if len(installConfig.OS) > 0 {
×
420
                currentOS := runtime.GOOS
×
421
                matches := false
×
422
                for _, osName := range installConfig.OS {
×
423
                        if osName == currentOS {
×
424
                                matches = true
×
425
                                break
×
426
                        }
427
                }
428
                if !matches {
×
429
                        return fmt.Errorf("install not supported on %s", currentOS)
×
430
                }
×
431
        }
432

433
        // 获取用户确认
434
        if !l.confirmInstall(skill.Name, installConfig) {
×
435
                return fmt.Errorf("install cancelled by user")
×
436
        }
×
437

438
        logger.Info("Installing dependency for skill",
×
439
                zap.String("skill", skill.Name),
×
440
                zap.String("binary", bin),
×
441
                zap.String("kind", installConfig.Kind))
×
442

×
443
        var cmd *exec.Cmd
×
444
        switch installConfig.Kind {
×
445
        case "brew":
×
446
                cmd = exec.Command("brew", "install", installConfig.Formula)
×
447
        case "apt", "apt-get":
×
448
                cmd = exec.Command("sudo", "apt-get", "install", "-y", installConfig.Formula)
×
449
        case "node":
×
450
                // node kind: use configured node package manager (npm, pnpm, yarn, bun)
×
451
                nodeManager := "npm" // default
×
452
                if nm := os.Getenv("GOCLAW_NODE_MANAGER"); nm != "" {
×
453
                        nodeManager = nm
×
454
                }
×
455
                cmd = exec.Command(nodeManager, "install", "-g", installConfig.Package)
×
456
        case "npm":
×
457
                cmd = exec.Command("npm", "install", "-g", installConfig.Package)
×
458
        case "pnpm":
×
459
                cmd = exec.Command("pnpm", "add", "-g", installConfig.Package)
×
460
        case "yarn":
×
461
                cmd = exec.Command("yarn", "global", installConfig.Package)
×
462
        case "bun":
×
463
                cmd = exec.Command("bun", "install", "-g", installConfig.Package)
×
464
        case "pip", "pip3":
×
465
                cmd = exec.Command("pip3", "install", installConfig.Package)
×
466
        case "uv":
×
467
                cmd = exec.Command("uv", "tool", "install", installConfig.Package)
×
468
        case "go":
×
469
                cmd = exec.Command("go", "install", installConfig.Package)
×
470
        case "command":
×
471
                cmd = exec.Command("sh", "-c", installConfig.Command)
×
472
        default:
×
473
                return fmt.Errorf("unsupported install kind: %s", installConfig.Kind)
×
474
        }
475

476
        // 执行安装,带超时
477
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
×
478
        defer cancel()
×
479

×
480
        output, err := cmd.CombinedOutput()
×
481
        _ = ctx // 避免未使用警告
×
482
        if err != nil {
×
483
                return fmt.Errorf("install failed: %w, output: %s", err, string(output))
×
484
        }
×
485

486
        // 刷新PATH
487
        if err := l.refreshPath(); err != nil {
×
488
                logger.Warn("Failed to refresh PATH after install", zap.Error(err))
×
489
        }
×
490

491
        logger.Info("Dependency installed successfully",
×
492
                zap.String("skill", skill.Name),
×
493
                zap.String("binary", bin))
×
494

×
495
        return nil
×
496
}
497

498
// findInstallConfig 查找安装配置
499
func (l *SkillsLoader) findInstallConfig(skill *Skill, bin string) *SkillInstall {
×
500
        // 首先匹配bins列表中的bin
×
501
        for _, install := range skill.Metadata.OpenClaw.Install {
×
502
                for _, providedBin := range install.Bins {
×
503
                        if providedBin == bin {
×
504
                                return &install
×
505
                        }
×
506
                }
507
        }
508
        // 匹配AnyBins
509
        for _, install := range skill.Metadata.OpenClaw.Install {
×
510
                for _, providedBin := range install.Bins {
×
511
                        if providedBin == bin {
×
512
                                return &install
×
513
                        }
×
514
                }
515
        }
516
        return nil
×
517
}
518

519
// confirmInstall 请求用户确认安装
520
func (l *SkillsLoader) confirmInstall(skillName string, install *SkillInstall) bool {
×
521
        // 如果是交互式终端,询问用户
×
522
        if l.isTerminal() {
×
523
                label := install.Label
×
524
                if label == "" {
×
525
                        label = fmt.Sprintf("Install %s (%s)", install.Kind, install.Formula)
×
526
                }
×
527
                fmt.Printf("\nSkill '%s' requires installing dependency:\n", skillName)
×
528
                fmt.Printf("  %s\n", label)
×
529
                fmt.Print("Install now? [Y/n]: ")
×
530

×
531
                var response string
×
NEW
532
                _, _ = fmt.Scanln(&response)
×
533
                return strings.ToLower(response) == "y" || response == ""
×
534
        }
535

536
        // 非交互式环境,自动安装
537
        return true
×
538
}
539

540
// isTerminal 检查是否在交互式终端
541
func (l *SkillsLoader) isTerminal() bool {
×
542
        stat, _ := os.Stdin.Stat()
×
543
        return (stat.Mode() & os.ModeCharDevice) != 0
×
544
}
×
545

546
// refreshPath 刷新PATH
547
func (l *SkillsLoader) refreshPath() error {
×
548
        homeDir, _ := os.UserHomeDir()
×
549

×
550
        // 获取当前shell路径并重新加载
×
551
        shellPaths := []string{
×
552
                "/bin",
×
553
                "/usr/bin",
×
554
                "/usr/local/bin",
×
555
                "/opt/homebrew/bin",
×
556
                "/opt/homebrew/opt/python3/bin",
×
557
        }
×
558

×
559
        // 添加 Node.js 包管理器全局安装路径
×
560
        if homeDir != "" {
×
561
                shellPaths = append(shellPaths,
×
562
                        homeDir+"/.npm-global/bin",           // npm
×
NEW
563
                        homeDir+"/.local/share/pnpm",         // pnpm
×
NEW
564
                        homeDir+"/.yarn/bin",                 // yarn
×
NEW
565
                        homeDir+"/.bun/bin",                  // bun
×
566
                        "/opt/homebrew/lib/node_modules/bin", // npm (brew-installed node)
×
567
                )
×
568
        }
×
569

570
        pathEnv := os.Getenv("PATH")
×
571
        if pathEnv == "" {
×
572
                pathEnv = strings.Join(shellPaths, ":")
×
573
        } else {
×
574
                pathEnv = pathEnv + ":" + strings.Join(shellPaths, ":")
×
575
        }
×
576
        os.Setenv("PATH", pathEnv)
×
577
        return nil
×
578
}
579

580
// PackageType 包类型枚举
581
type PackageType string
582

583
const (
584
        PackageTypePython PackageType = "python"
585
        PackageTypeNode   PackageType = "node"
586
)
587

588
// checkPackageInstalled 检查包是否已安装(通用函数)
589
func (l *SkillsLoader) checkPackageInstalled(pkgType PackageType, pkg string) error {
×
590
        switch pkgType {
×
591
        case PackageTypePython:
×
592
                cmd := exec.Command("python3", "-c", fmt.Sprintf("import %s; print('OK')", pkg))
×
593
                output, err := cmd.CombinedOutput()
×
594
                if err != nil || !strings.Contains(string(output), "OK") {
×
595
                        return fmt.Errorf("Python package not found: %s", pkg)
×
596
                }
×
597
                return nil
×
598
        case PackageTypeNode:
×
599
                cmd := exec.Command("npm", "list", "--global", "--json", "--depth=0", pkg)
×
600
                output, err := cmd.CombinedOutput()
×
601
                if err != nil {
×
602
                        return fmt.Errorf("npm command failed: %w", err)
×
603
                }
×
604
                var result []npmPackageInfo
×
605
                if err := json.Unmarshal(output, &result); err != nil {
×
606
                        return fmt.Errorf("failed to parse npm output: %w", err)
×
607
                }
×
608
                if len(result) == 0 {
×
609
                        return fmt.Errorf("Node.js package not found: %s", pkg)
×
610
                }
×
611
                return nil
×
612
        default:
×
613
                return fmt.Errorf("unsupported package type: %s", pkgType)
×
614
        }
615
}
616

617
// tryInstallPackage 尝试安装包(通用函数)
618
func (l *SkillsLoader) tryInstallPackage(pkgType PackageType, pkg string) error {
×
619
        logger.Info("Installing package", zap.String("type", string(pkgType)), zap.String("package", pkg))
×
620

×
621
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
×
622
        defer cancel()
×
623

×
624
        var cmd *exec.Cmd
×
625
        switch pkgType {
×
626
        case PackageTypePython:
×
627
                cmd = exec.CommandContext(ctx, "python3", "-m", "pip", "install", pkg)
×
628
        case PackageTypeNode:
×
629
                cmd = exec.CommandContext(ctx, "npm", "install", "-g", pkg)
×
630
        default:
×
631
                return fmt.Errorf("unsupported package type: %s", pkgType)
×
632
        }
633

634
        output, err := cmd.CombinedOutput()
×
635
        if err != nil {
×
636
                return fmt.Errorf("%s install failed: %w, output: %s", pkgType, err, string(output))
×
637
        }
×
638

639
        logger.Info("Package installed successfully", zap.String("type", string(pkgType)), zap.String("package", pkg))
×
640
        return nil
×
641
}
642

643
// checkPythonPackage 检查Python包是否已安装
644
func (l *SkillsLoader) checkPythonPackage(pkg string) error {
×
645
        return l.checkPackageInstalled(PackageTypePython, pkg)
×
646
}
×
647

648
// npmPackageInfo npm包信息
649
type npmPackageInfo struct {
650
        Name string `json:"name"`
651
}
652

653
// checkNodePackage 检查Node.js包是否已安装
654
func (l *SkillsLoader) checkNodePackage(pkg string) error {
×
655
        return l.checkPackageInstalled(PackageTypeNode, pkg)
×
656
}
×
657

658
// tryInstallPythonPackage 尝试安装Python包
659
func (l *SkillsLoader) tryInstallPythonPackage(pkg string) error {
×
660
        return l.tryInstallPackage(PackageTypePython, pkg)
×
661
}
×
662

663
// tryInstallNodePackage 尝试安装Node.js包
664
func (l *SkillsLoader) tryInstallNodePackage(pkg string) error {
×
665
        return l.tryInstallPackage(PackageTypeNode, pkg)
×
666
}
×
667

668
// getMissingDeps 计算缺失的依赖
669
func (l *SkillsLoader) getMissingDeps(skill *Skill) *MissingDeps {
×
670
        var missing MissingDeps
×
671

×
672
        // 检查二进制文件
×
673
        for _, bin := range skill.Metadata.OpenClaw.Requires.Bins {
×
674
                if _, err := exec.LookPath(bin); err != nil {
×
675
                        missing.Bins = append(missing.Bins, bin)
×
676
                }
×
677
        }
678

679
        // 检查 AnyBins
680
        for _, bin := range skill.Metadata.OpenClaw.Requires.AnyBins {
×
681
                found := false
×
682
                for _, b := range skill.Metadata.OpenClaw.Requires.AnyBins {
×
683
                        if _, err := exec.LookPath(b); err == nil {
×
684
                                found = true
×
685
                                break
×
686
                        }
687
                }
688
                if !found {
×
689
                        missing.AnyBins = append(missing.AnyBins, bin)
×
690
                }
×
691
        }
692

693
        // 检查Python包
694
        for _, pkg := range skill.Metadata.OpenClaw.Requires.PythonPkgs {
×
695
                if err := l.checkPythonPackage(pkg); err != nil {
×
696
                        missing.PythonPkgs = append(missing.PythonPkgs, pkg)
×
697
                }
×
698
        }
699

700
        // 检查Node.js包
701
        for _, pkg := range skill.Metadata.OpenClaw.Requires.NodePkgs {
×
702
                if err := l.checkNodePackage(pkg); err != nil {
×
703
                        missing.NodePkgs = append(missing.NodePkgs, pkg)
×
704
                }
×
705
        }
706

707
        // 检查环境变量
708
        for _, env := range skill.Metadata.OpenClaw.Requires.Env {
×
709
                if os.Getenv(env) == "" {
×
710
                        missing.Env = append(missing.Env, env)
×
711
                }
×
712
        }
713
        for _, env := range skill.Requires.Env {
×
714
                if os.Getenv(env) == "" {
×
715
                        missing.Env = append(missing.Env, env)
×
716
                }
×
717
        }
718

719
        // 如果没有缺失依赖,返回nil
720
        if len(missing.Bins) == 0 &&
×
721
                len(missing.AnyBins) == 0 &&
×
722
                len(missing.PythonPkgs) == 0 &&
×
723
                len(missing.NodePkgs) == 0 &&
×
724
                len(missing.Env) == 0 {
×
725
                return nil
×
726
        }
×
727

728
        return &missing
×
729
}
730

731
// SearchResult 搜索结果
732
type SearchResult struct {
733
        Skill   *Skill
734
        Source  string // skill的来源路径
735
        Score   float64
736
        Matches []string // 匹配的字段
737
}
738

739
// Search 搜索技能
740
func (l *SkillsLoader) Search(query string) []*SearchResult {
×
741
        if len(l.skills) == 0 {
×
742
                return nil
×
743
        }
×
744

745
        query = strings.ToLower(query)
×
746
        var results []*SearchResult
×
747

×
748
        for name, skill := range l.skills {
×
749
                score := 0.0
×
750
                var matches []string
×
751

×
752
                // 检查名称匹配
×
753
                if strings.Contains(strings.ToLower(name), query) {
×
754
                        // 精确匹配得分更高
×
755
                        if strings.EqualFold(name, query) {
×
756
                                score += 1.0
×
757
                                matches = append(matches, "name (exact)")
×
758
                        } else {
×
759
                                score += 0.8
×
760
                                matches = append(matches, "name")
×
761
                        }
×
762
                }
763

764
                // 检查描述匹配
765
                lowerDesc := strings.ToLower(skill.Description)
×
766
                if strings.Contains(lowerDesc, query) {
×
767
                        score += 0.6
×
768
                        matches = append(matches, "description")
×
769
                }
×
770

771
                // 检查作者匹配
772
                if strings.Contains(strings.ToLower(skill.Author), query) {
×
773
                        score += 0.4
×
774
                        matches = append(matches, "author")
×
775
                }
×
776

777
                // 检查内容匹配(内容太长,只按关键词查找)
778
                keywords := strings.Fields(query)
×
779
                lowerContent := strings.ToLower(skill.Content)
×
780
                contentMatches := 0
×
781
                for _, keyword := range keywords {
×
782
                        if strings.Contains(lowerContent, strings.ToLower(keyword)) {
×
783
                                contentMatches++
×
784
                        }
×
785
                }
786
                if contentMatches > 0 {
×
787
                        contentScore := 0.3 * float64(contentMatches) / float64(len(keywords))
×
788
                        score += contentScore
×
789
                        if contentMatches == len(keywords) {
×
790
                                matches = append(matches, "content")
×
791
                        }
×
792
                }
793

794
                // 只返回有匹配的结果
795
                if score > 0 {
×
796
                        results = append(results, &SearchResult{
×
797
                                Skill:   skill,
×
798
                                Source:  resolveSkillSource(skill),
×
799
                                Score:   score,
×
800
                                Matches: matches,
×
801
                        })
×
802
                }
×
803
        }
804

805
        // 按得分排序
806
        if len(results) > 0 {
×
807
                sort.Slice(results, func(i, j int) bool {
×
808
                        return results[i].Score > results[j].Score
×
809
                })
×
810
        }
811

812
        return results
×
813
}
814

815
// resolveSkillSource 解析技能来源
816
func resolveSkillSource(skill *Skill) string {
×
817
        // 检查是否来自远程仓库
×
818
        if strings.Contains(skill.Homepage, "github.com") || strings.Contains(skill.Homepage, "gitlab.com") {
×
819
                return "remote"
×
820
        }
×
821

822
        // 检查是否来自本地路径
823
        if skill.Homepage != "" && (strings.HasPrefix(skill.Homepage, "/") || strings.HasPrefix(skill.Homepage, ".")) {
×
824
                return "local"
×
825
        }
×
826

827
        return "builtin"
×
828
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc