sdaduanbilei fe1a82bbe2 feat: Plan/Build 模式、artifact 系统、成员直接对话
- 新增 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>
2026-03-06 17:34:44 +08:00

100 lines
2.4 KiB
Go
Raw 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"
"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]))
}