Files
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

128 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package skills 实现 SKILL.md 技能加载(借鉴 ccb docs/extensibility/skills 与
// picoclaw pkg/skills):skill = 目录下的 SKILL.md(YAML frontmatter + 正文指令),
// 不是代码。系统提示只注入 name+description 列表(带预算截断),正文由模型
// 通过 load_skill 工具按需拉取,节省上下文。
package skills
import (
"fmt"
"os"
"path/filepath"
"strings"
"git.gopher.ink/mirrors2/AutoZero/textx"
)
// Skill 是一个已解析的技能。
type Skill struct {
Name string // frontmatter name,缺省用目录名
Description string // 何时该用这个技能
AllowedTools []string // 可选:激活后允许使用的工具白名单
Body string // SKILL.md 正文(Markdown 指令)
Dir string // 技能目录绝对路径
}
// Load 依次扫描多个技能根目录(每个目录下是 <skill-name>/SKILL.md)。
// 同名技能先到先得——调用方把高优先级目录放前面(项目级覆盖用户级)。
// 单个坏文件只跳过,不影响其余。
func Load(dirs ...string) []Skill {
var out []Skill
seen := map[string]bool{}
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, e := range entries {
if !e.IsDir() {
continue
}
path := filepath.Join(dir, e.Name(), "SKILL.md")
data, err := os.ReadFile(path)
if err != nil {
continue
}
meta, body := parseFrontmatter(string(data))
name := meta["name"]
if name == "" {
name = e.Name()
}
if seen[name] {
continue
}
seen[name] = true
s := Skill{
Name: name,
Description: meta["description"],
Body: strings.TrimSpace(body),
Dir: filepath.Join(dir, e.Name()),
}
if v := meta["allowed-tools"]; v != "" {
for _, t := range strings.Split(v, ",") {
if t = strings.TrimSpace(t); t != "" {
s.AllowedTools = append(s.AllowedTools, t)
}
}
}
out = append(out, s)
}
}
return out
}
// maxPromptChars 是技能列表注入系统提示的预算(借鉴 ccb 的 1% 上下文预算思路)。
const maxPromptChars = 4000
// PromptSection 生成注入系统提示的技能列表段;空列表返回空串。
func PromptSection(sk []Skill) string {
if len(sk) == 0 {
return ""
}
var b strings.Builder
b.WriteString("\n# Skills\nThe following skills are available. Before doing a task that matches a skill, call the load_skill tool with its name and follow the returned instructions.\n")
for _, s := range sk {
line := "- " + s.Name
if s.Description != "" {
line += ": " + s.Description
}
if len(s.AllowedTools) > 0 {
line += fmt.Sprintf(" (allowed-tools: %s)", strings.Join(s.AllowedTools, ", "))
}
b.WriteString(line + "\n")
}
out := b.String()
if len(out) > maxPromptChars {
// 技能描述大量是中文:按字节切会在尾部留下半个字,而这段是 system prompt。
out = textx.ClipBytes(out, maxPromptChars) + "\n...[skill list truncated]\n"
}
return out
}
// parseFrontmatter 解析开头的 `---` YAML 块,只支持顶层 `key: value` 行
// (足够覆盖 name/description/allowed-tools,避免引入 YAML 依赖)。
// 返回 (meta, 正文)。
func parseFrontmatter(raw string) (map[string]string, string) {
s := strings.TrimSpace(raw)
if !strings.HasPrefix(s, "---") {
return map[string]string{}, raw
}
rel := strings.Index(s[3:], "\n---")
if rel < 0 {
return map[string]string{}, raw
}
meta := map[string]string{}
for _, line := range strings.Split(s[3:3+rel], "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, ":")
if !ok {
continue
}
meta[strings.TrimSpace(k)] = strings.Trim(strings.TrimSpace(v), `"'`)
}
body := s[3+rel+4:] // 跳过 "\n---"
return meta, strings.TrimLeft(body, "\n")
}