scorpio d6df056687 feat: Part 模型 + 文件版本追踪 + 写手团队工作流 v2
- 数据层: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>
2026-03-08 18:44:34 +08:00

143 lines
3.6 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 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 ""
}