refactor: 项目结构重组,src/ 扁平化为根目录,提取 pkg/ 子包
- 模块名重命名 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*
This commit is contained in:
311
catalog.go
Normal file
311
catalog.go
Normal file
@@ -0,0 +1,311 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user