package services import ( "os" "path/filepath" "strings" "sproutclaw-web/internal/models" ) // builtinCommands are the slash commands the WebUI handles natively. var builtinCommands = []models.SlashCommand{ {Name: "new", Description: "开启新会话", Source: "builtin"}, {Name: "compact", Description: "压缩会话历史", Source: "builtin"}, {Name: "reload", Description: "重载配置", Source: "builtin"}, {Name: "clone", Description: "克隆当前会话", Source: "builtin"}, {Name: "name", Description: "重命名当前会话", Source: "builtin"}, {Name: "model", Description: "切换模型", Source: "builtin"}, {Name: "session", Description: "显示会话统计", Source: "builtin"}, {Name: "export", Description: "导出为 HTML", Source: "builtin"}, {Name: "copy", Description: "复制最后一条助手消息", Source: "builtin"}, {Name: "webui", Description: "WebUI 控制", Source: "builtin"}, } // ListSlashCommands aggregates all slash commands visible in the WebUI. func ListSlashCommands(agentDir string) []models.SlashCommand { cmds := make([]models.SlashCommand, len(builtinCommands)) copy(cmds, builtinCommands) // scan skills dir for SKILL.md files skillsDir := filepath.Join(agentDir, "skills") cmds = append(cmds, scanSkillCommands(skillsDir)...) return cmds } func scanSkillCommands(skillsDir string) []models.SlashCommand { entries, err := os.ReadDir(skillsDir) if err != nil { return nil } var cmds []models.SlashCommand for _, e := range entries { if !e.IsDir() { continue } skillMd := filepath.Join(skillsDir, e.Name(), "SKILL.md") name, desc := parseSkillMD(skillMd) if name == "" { name = e.Name() } cmds = append(cmds, models.SlashCommand{ Name: name, Description: desc, Source: "skill", }) } return cmds } func parseSkillMD(path string) (name, desc string) { data, err := os.ReadFile(path) if err != nil { return "", "" } content := string(data) // parse frontmatter between --- delimiters if !strings.HasPrefix(content, "---") { return "", "" } parts := strings.SplitN(content, "---", 3) if len(parts) < 3 { return "", "" } fm := parts[1] for _, line := range strings.Split(fm, "\n") { kv := strings.SplitN(line, ":", 2) if len(kv) != 2 { continue } k := strings.TrimSpace(kv[0]) v := strings.TrimSpace(kv[1]) switch k { case "name": name = v case "description": desc = v } } return name, desc }