- 数据层:messages 表增加 part_type 字段,新建 file_versions 表支持版本追踪 - 后端:saveWorkspace 版本追踪、saveAgentOutput 源头分离、generateBriefMessage 成员简报 - 后端:applyDocumentEdit 增量编辑、buildWorkflowStep phase-aware 工作流引擎 - API:文件版本查询/回退接口 - 前端:part_type 驱动渲染,产物面板版本历史 - 新增写手团队(主编/搜索员/策划编辑/合规审查员)配置 - store 模块、scheduler 模块、web-search skill Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
3.6 KiB
Go
143 lines
3.6 KiB
Go
package skill
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/sdaduanbilei/agent-team/internal/llm"
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
type Meta struct {
|
||
Name string `yaml:"name"`
|
||
Description string `yaml:"description"`
|
||
Path string `yaml:"-"`
|
||
}
|
||
|
||
type Skill struct {
|
||
Meta
|
||
Body string // full SKILL.md body (instructions)
|
||
}
|
||
|
||
// Discover 递归扫描 skillsDir,返回所有包含 SKILL.md 的目录的元数据。
|
||
func Discover(skillsDir string) ([]Meta, error) {
|
||
var metas []Meta
|
||
filepath.Walk(skillsDir, func(path string, info os.FileInfo, err error) error {
|
||
if err != nil || info.IsDir() || info.Name() != "SKILL.md" {
|
||
return nil
|
||
}
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
meta, err := ParseMeta(data)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
meta.Path = filepath.Dir(path)
|
||
metas = append(metas, meta)
|
||
return nil
|
||
})
|
||
return metas, nil
|
||
}
|
||
|
||
// Load returns a fully loaded skill including body.
|
||
func Load(skillDir string) (*Skill, error) {
|
||
data, err := os.ReadFile(filepath.Join(skillDir, "SKILL.md"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
meta, err := ParseMeta(data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
meta.Path = skillDir
|
||
body := extractBody(data)
|
||
return &Skill{Meta: meta, Body: body}, nil
|
||
}
|
||
|
||
// ToXML generates <available_skills> XML for agent system prompts.
|
||
// 如果 skill 有完整内容,会注入到 prompt 中。
|
||
func ToXML(metas []Meta) string {
|
||
var sb strings.Builder
|
||
sb.WriteString("<available_skills>\n")
|
||
for _, m := range metas {
|
||
// 尝试加载完整 skill 内容
|
||
s, err := Load(m.Path)
|
||
if err == nil && s.Body != "" {
|
||
fmt.Fprintf(&sb, " <skill name=\"%s\">\n <description>%s</description>\n <instructions>\n%s\n </instructions>\n </skill>\n",
|
||
m.Name, m.Description, s.Body)
|
||
} else {
|
||
fmt.Fprintf(&sb, " <skill name=\"%s\">\n <description>%s</description>\n </skill>\n",
|
||
m.Name, m.Description)
|
||
}
|
||
}
|
||
sb.WriteString("</available_skills>")
|
||
return sb.String()
|
||
}
|
||
|
||
func ParseMeta(data []byte) (Meta, error) {
|
||
var meta Meta
|
||
if !bytes.HasPrefix(data, []byte("---")) {
|
||
return meta, fmt.Errorf("missing frontmatter")
|
||
}
|
||
parts := bytes.SplitN(data, []byte("---"), 3)
|
||
if len(parts) < 3 {
|
||
return meta, fmt.Errorf("invalid frontmatter")
|
||
}
|
||
return meta, yaml.Unmarshal(parts[1], &meta)
|
||
}
|
||
|
||
func extractBody(data []byte) string {
|
||
parts := bytes.SplitN(data, []byte("---"), 3)
|
||
if len(parts) < 3 {
|
||
return string(data)
|
||
}
|
||
return strings.TrimSpace(string(parts[2]))
|
||
}
|
||
|
||
// ToTools 将 skill 列表转为 LLM tool 定义。
|
||
// 每个有 scripts/ 目录的 skill 生成一个 tool。
|
||
func ToTools(metas []Meta) []llm.Tool {
|
||
var tools []llm.Tool
|
||
for _, m := range metas {
|
||
scriptsDir := filepath.Join(m.Path, "scripts")
|
||
if _, err := os.Stat(scriptsDir); err != nil {
|
||
continue // 没有 scripts 目录的 skill 不注册为 tool
|
||
}
|
||
tools = append(tools, llm.Tool{
|
||
Type: llm.ToolTypeFunction,
|
||
Function: &llm.FunctionDefinition{
|
||
Name: "skill_" + strings.ReplaceAll(m.Name, "-", "_"),
|
||
Description: m.Description,
|
||
Parameters: json.RawMessage(`{
|
||
"type": "object",
|
||
"properties": {
|
||
"command": {
|
||
"type": "string",
|
||
"description": "要执行的完整 bash 命令,例如: bash \"$SKILLS_ROOT/` + m.Name + `/scripts/search.sh\" \"搜索关键词\" 5"
|
||
}
|
||
},
|
||
"required": ["command"]
|
||
}`),
|
||
},
|
||
})
|
||
}
|
||
return tools
|
||
}
|
||
|
||
// SkillPathByToolName 根据 tool 函数名找到对应 skill 的路径
|
||
func SkillPathByToolName(metas []Meta, funcName string) string {
|
||
for _, m := range metas {
|
||
toolName := "skill_" + strings.ReplaceAll(m.Name, "-", "_")
|
||
if toolName == funcName {
|
||
return m.Path
|
||
}
|
||
}
|
||
return ""
|
||
}
|