- 新增 Plan/Build 模式切换(Tab 键),Plan 模式阻止任务执行 - Build 模式:成员输出智能判断,文档存 artifact,提问显示聊天 - 成员可直接与用户对话(多轮),不经过 master 传话 - 任务计划文档自动生成,沟通记录自动追加 - 右侧面板重构为产出物面板,支持查看/编辑/保存 - 输入框改为 textarea,支持 Shift+Enter 换行,修复输入法 Enter 误发送 - Master 会话历史持久化,支持多轮上下文 - parseAssignments 支持多行任务描述 - SOUL.md 热重载、skill 递归发现与内容注入 - 需求确认 skill(HARD-GATE 模式) - air 热重载配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
2.4 KiB
Go
100 lines
2.4 KiB
Go
package skill
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"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]))
|
||
}
|