- 模块名重命名 yunshu -> hub.gaomia.site/titor/YunShu - Go 版本升级 1.21 -> 1.25 - src/ 目录删除,所有文件移至根目录 - 新增 pkg/mdprint/: Markdown AST 解析+ANSI 渲染 - 新增 pkg/style/: 终端颜色样式(8色 ANSI + 24位真彩色) - 新增 pkg/termui/: 终端输入组件(交互式输入/密码/确认) - 更新文档:AGENTS.md、architecture.md、changelog.md、taolun.md - gitignore 通配符修复 yunshu.exe -> yunshu.exe*
312 lines
7.5 KiB
Go
312 lines
7.5 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
// ============================================================
|
||
// YAML 目录结构
|
||
// ============================================================
|
||
|
||
type ToolsCatalog struct {
|
||
Version string `yaml:"version"`
|
||
Auto CatalogSection `yaml:"auto"`
|
||
Manual CatalogSection `yaml:"manual,omitempty"`
|
||
}
|
||
|
||
type CatalogSection struct {
|
||
Tools []CatalogTool `yaml:"tools"`
|
||
Skills []CatalogSkill `yaml:"skills"`
|
||
Agents []CatalogAgent `yaml:"agents"`
|
||
}
|
||
|
||
type CatalogTool struct {
|
||
Name string `yaml:"name"`
|
||
Type string `yaml:"type"`
|
||
Status string `yaml:"status"`
|
||
Description string `yaml:"description"`
|
||
Parameters map[string]ParameterField `yaml:"parameters"`
|
||
Returns string `yaml:"returns"`
|
||
Source string `yaml:"source"`
|
||
}
|
||
|
||
type ParameterField struct {
|
||
Type string `yaml:"type"`
|
||
Required bool `yaml:"required"`
|
||
Description string `yaml:"description"`
|
||
}
|
||
|
||
type CatalogSkill struct {
|
||
Name string `yaml:"name"`
|
||
Path string `yaml:"path"`
|
||
Description string `yaml:"description"`
|
||
Status string `yaml:"status"`
|
||
}
|
||
|
||
type CatalogAgent struct {
|
||
Name string `yaml:"name"`
|
||
Path string `yaml:"path"`
|
||
Description string `yaml:"description"`
|
||
Tools []string `yaml:"tools"`
|
||
Status string `yaml:"status"`
|
||
}
|
||
|
||
// ============================================================
|
||
// 构建目录
|
||
// ============================================================
|
||
|
||
func BuildCatalog() *ToolsCatalog {
|
||
return &ToolsCatalog{
|
||
Version: "1.0",
|
||
Auto: CatalogSection{
|
||
Tools: buildToolList(),
|
||
Skills: scanSkills(),
|
||
Agents: scanAgents(),
|
||
},
|
||
}
|
||
}
|
||
|
||
func buildToolList() []CatalogTool {
|
||
tools := ListRegisteredTools()
|
||
list := make([]CatalogTool, 0, len(tools))
|
||
for _, t := range tools {
|
||
ct := CatalogTool{
|
||
Name: t.Name,
|
||
Type: "builtin",
|
||
Status: "active",
|
||
Description: t.Description,
|
||
Source: "src/tool.go",
|
||
}
|
||
|
||
// 从 JSON Schema 提取参数
|
||
ct.Parameters = make(map[string]ParameterField)
|
||
for name, prop := range t.Parameters.Properties {
|
||
required := false
|
||
for _, r := range t.Parameters.Required {
|
||
if r == name {
|
||
required = true
|
||
break
|
||
}
|
||
}
|
||
ct.Parameters[name] = ParameterField{
|
||
Type: prop.Type,
|
||
Required: required,
|
||
Description: prop.Description,
|
||
}
|
||
}
|
||
list = append(list, ct)
|
||
}
|
||
return list
|
||
}
|
||
|
||
func scanSkills() []CatalogSkill {
|
||
// 搜索路径:项目目录 → 用户配置目录
|
||
dirs := []string{
|
||
"skills",
|
||
filepath.Join(ConfigDir(), "skills"),
|
||
}
|
||
|
||
seen := make(map[string]bool)
|
||
var list []CatalogSkill
|
||
|
||
for _, dir := range dirs {
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, e := range entries {
|
||
if !e.IsDir() || seen[e.Name()] {
|
||
continue
|
||
}
|
||
seen[e.Name()] = true
|
||
|
||
// 尝试读取 SKILL.md 获取描述
|
||
skillPath := filepath.Join(dir, e.Name(), "SKILL.md")
|
||
desc := fmt.Sprintf("skill: %s", e.Name())
|
||
|
||
if data, err := os.ReadFile(skillPath); err == nil {
|
||
content := string(data)
|
||
if fm, _, err := parseFrontmatterSimple(content); err == nil {
|
||
if d, ok := fm["description"]; ok {
|
||
desc = fmt.Sprintf("%v", d)
|
||
}
|
||
}
|
||
}
|
||
|
||
list = append(list, CatalogSkill{
|
||
Name: e.Name(),
|
||
Path: fmt.Sprintf("skills/%s/SKILL.md", e.Name()),
|
||
Description: desc,
|
||
Status: "active",
|
||
})
|
||
}
|
||
}
|
||
return list
|
||
}
|
||
|
||
func scanAgents() []CatalogAgent {
|
||
dirs := []string{
|
||
"agents",
|
||
filepath.Join(ConfigDir(), "agents"),
|
||
}
|
||
|
||
seen := make(map[string]bool)
|
||
var list []CatalogAgent
|
||
|
||
for _, dir := range dirs {
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, e := range entries {
|
||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || seen[e.Name()] {
|
||
continue
|
||
}
|
||
seen[e.Name()] = true
|
||
|
||
agentPath := filepath.Join(dir, e.Name())
|
||
data, err := os.ReadFile(agentPath)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
fm, _, err := parseFrontmatterSimple(string(data))
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
cat := CatalogAgent{
|
||
Name: fmt.Sprintf("%v", fm["name"]),
|
||
Path: fmt.Sprintf("agents/%s", e.Name()),
|
||
Status: "active",
|
||
}
|
||
|
||
if d, ok := fm["description"]; ok {
|
||
cat.Description = fmt.Sprintf("%v", d)
|
||
}
|
||
if tools, ok := fm["tools"]; ok {
|
||
if list, ok := tools.([]interface{}); ok {
|
||
for _, t := range list {
|
||
cat.Tools = append(cat.Tools, fmt.Sprintf("%v", t))
|
||
}
|
||
}
|
||
}
|
||
|
||
list = append(list, cat)
|
||
}
|
||
}
|
||
return list
|
||
}
|
||
|
||
// ============================================================
|
||
// 写入 tools.yml
|
||
// ============================================================
|
||
|
||
func GenerateToolsYAML() {
|
||
catalog := BuildCatalog()
|
||
|
||
// 尝试读取已有的 manual 节,保留用户编辑
|
||
existingPath := filepath.Join(ConfigDir(), "tools.yml")
|
||
if data, err := os.ReadFile(existingPath); err == nil {
|
||
var existing ToolsCatalog
|
||
if err := yaml.Unmarshal(data, &existing); err == nil {
|
||
catalog.Manual = existing.Manual
|
||
}
|
||
}
|
||
|
||
dir := ConfigDir()
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return
|
||
}
|
||
|
||
// 用 2 空格缩进编码
|
||
var buf bytes.Buffer
|
||
encoder := yaml.NewEncoder(&buf)
|
||
encoder.SetIndent(2)
|
||
if err := encoder.Encode(catalog); err != nil {
|
||
return
|
||
}
|
||
encoder.Close()
|
||
|
||
// 在文件头加注释
|
||
header := "# ⚠️ auto 节由云枢 Agent 自动生成,每次启动覆写;manual 节保留用户自定义\n"
|
||
yamlPath := filepath.Join(dir, "tools.yml")
|
||
os.WriteFile(yamlPath, []byte(header+buf.String()), 0644)
|
||
}
|
||
|
||
// ============================================================
|
||
// 注入目录到 system prompt
|
||
// ============================================================
|
||
|
||
// BuildInjectPrompt 生成能力边界目录,追加到 system prompt 末尾
|
||
func BuildInjectPrompt(toolNames []string) string {
|
||
var b strings.Builder
|
||
b.WriteString("\n\n## 能力边界\n\n")
|
||
|
||
// 工具
|
||
allTools := ListRegisteredTools()
|
||
b.WriteString("### 可用工具\n")
|
||
for _, t := range allTools {
|
||
available := false
|
||
for _, name := range toolNames {
|
||
if t.Name == name {
|
||
available = true
|
||
break
|
||
}
|
||
}
|
||
if available {
|
||
b.WriteString(fmt.Sprintf("- %s: %s\n", t.Name, t.Description))
|
||
}
|
||
}
|
||
|
||
// skill
|
||
skills := scanSkills()
|
||
if len(skills) > 0 {
|
||
b.WriteString("\n### 可用技能\n")
|
||
for _, s := range skills {
|
||
b.WriteString(fmt.Sprintf("- %s: %s\n", s.Name, s.Description))
|
||
}
|
||
}
|
||
|
||
// 数据文件
|
||
b.WriteString("\n### 数据文件\n")
|
||
b.WriteString("- data/tools.yml: 完整工具/技能/Agent 目录(可读此文件获取详细参数)\n")
|
||
|
||
return b.String()
|
||
}
|
||
|
||
// ============================================================
|
||
// 辅助:简易 frontmatter 解析(只读 YAML map)
|
||
// ============================================================
|
||
|
||
func parseFrontmatterSimple(content string) (map[string]interface{}, string, error) {
|
||
lines := strings.Split(content, "\n")
|
||
if len(lines) < 2 || strings.TrimSpace(lines[0]) != "---" {
|
||
return nil, content, nil
|
||
}
|
||
endIdx := -1
|
||
for i := 1; i < len(lines); i++ {
|
||
if strings.TrimSpace(lines[i]) == "---" {
|
||
endIdx = i
|
||
break
|
||
}
|
||
}
|
||
if endIdx == -1 {
|
||
return nil, content, nil
|
||
}
|
||
frontmatter := strings.Join(lines[1:endIdx], "\n")
|
||
body := strings.Join(lines[endIdx+1:], "\n")
|
||
|
||
var fm map[string]interface{}
|
||
if err := yaml.Unmarshal([]byte(frontmatter), &fm); err != nil {
|
||
return nil, body, nil
|
||
}
|
||
return fm, body, nil
|
||
}
|