🎉 初始化项目
This commit is contained in:
549
server/service/system/sys_skills.go
Normal file
549
server/service/system/sys_skills.go
Normal file
@@ -0,0 +1,549 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/model/system"
|
||||
"git.echol.cn/loser/st/server/model/system/request"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
skillFileName = "SKILL.md"
|
||||
globalConstraintFileName = "README.md"
|
||||
)
|
||||
|
||||
var skillToolOrder = []string{"copilot", "claude", "cursor", "trae", "codex"}
|
||||
|
||||
var skillToolDirs = map[string]string{
|
||||
"copilot": ".aone_copilot",
|
||||
"claude": ".claude",
|
||||
"trae": ".trae",
|
||||
"codex": ".codex",
|
||||
"cursor": ".cursor",
|
||||
}
|
||||
|
||||
var skillToolLabels = map[string]string{
|
||||
"copilot": "Copilot",
|
||||
"claude": "Claude",
|
||||
"trae": "Trae",
|
||||
"codex": "Codex",
|
||||
"cursor": "Cursor",
|
||||
}
|
||||
|
||||
const defaultSkillMarkdown = "## 技能用途\n请在这里描述技能的目标、适用场景与限制条件。\n\n## 输入\n- 请补充输入格式与示例。\n\n## 输出\n- 请补充输出格式与示例。\n\n## 关键步骤\n1. 第一步\n2. 第二步\n\n## 示例\n在此补充一到两个典型示例。\n"
|
||||
|
||||
const defaultResourceMarkdown = "# 资源说明\n请在这里补充资源内容。\n"
|
||||
|
||||
const defaultReferenceMarkdown = "# 参考资料\n请在这里补充参考资料内容。\n"
|
||||
|
||||
const defaultTemplateMarkdown = "# 模板\n请在这里补充模板内容。\n"
|
||||
|
||||
const defaultGlobalConstraintMarkdown = "# 全局约束\n请在这里补充该工具的统一约束与使用规范。\n"
|
||||
|
||||
type SkillsService struct{}
|
||||
|
||||
func (s *SkillsService) Tools(_ context.Context) ([]system.SkillTool, error) {
|
||||
tools := make([]system.SkillTool, 0, len(skillToolOrder))
|
||||
for _, key := range skillToolOrder {
|
||||
if _, err := s.toolSkillsDir(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tools = append(tools, system.SkillTool{Key: key, Label: skillToolLabels[key]})
|
||||
}
|
||||
return tools, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) List(_ context.Context, tool string) ([]string, error) {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(skillsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var skills []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
skills = append(skills, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(skills)
|
||||
return skills, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) Detail(_ context.Context, tool, skill string) (system.SkillDetail, error) {
|
||||
var detail system.SkillDetail
|
||||
if !isSafeName(skill) {
|
||||
return detail, errors.New("技能名称不合法")
|
||||
}
|
||||
detail.Tool = tool
|
||||
detail.Skill = skill
|
||||
|
||||
skillDir, err := s.skillDir(tool, skill)
|
||||
if err != nil {
|
||||
return detail, err
|
||||
}
|
||||
|
||||
skillFilePath := filepath.Join(skillDir, skillFileName)
|
||||
content, err := os.ReadFile(skillFilePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return detail, err
|
||||
}
|
||||
detail.Meta = system.SkillMeta{Name: skill}
|
||||
detail.Markdown = defaultSkillMarkdown
|
||||
} else {
|
||||
meta, body, parseErr := parseSkillContent(string(content))
|
||||
if parseErr != nil {
|
||||
meta = system.SkillMeta{Name: skill}
|
||||
body = string(content)
|
||||
}
|
||||
if meta.Name == "" {
|
||||
meta.Name = skill
|
||||
}
|
||||
detail.Meta = meta
|
||||
detail.Markdown = body
|
||||
}
|
||||
|
||||
detail.Scripts = listFiles(filepath.Join(skillDir, "scripts"))
|
||||
detail.Resources = listFiles(filepath.Join(skillDir, "resources"))
|
||||
detail.References = listFiles(filepath.Join(skillDir, "references"))
|
||||
detail.Templates = listFiles(filepath.Join(skillDir, "templates"))
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) error {
|
||||
if !isSafeName(req.Skill) {
|
||||
return errors.New("技能名称不合法")
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Meta.Name == "" {
|
||||
req.Meta.Name = req.Skill
|
||||
}
|
||||
content, err := buildSkillContent(req.Meta, req.Markdown)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(skillDir, skillFileName), []byte(content), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.SyncTools) > 0 {
|
||||
for _, tool := range req.SyncTools {
|
||||
if tool == req.Tool {
|
||||
continue
|
||||
}
|
||||
targetDir, err := s.ensureSkillDir(tool, req.Skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copySkillDir(skillDir, targetDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) {
|
||||
if !isSafeName(req.Skill) {
|
||||
return "", "", errors.New("技能名称不合法")
|
||||
}
|
||||
fileName, lang, err := buildScriptFileName(req.FileName, req.ScriptType)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if lang == "" {
|
||||
return "", "", errors.New("脚本类型不支持")
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, "scripts", fileName)
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
return "", "", errors.New("脚本已存在")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
content := scriptTemplate(lang)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return fileName, content, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetScript(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "scripts", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveScript(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "scripts", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateResource(_ context.Context, req request.SkillResourceCreateRequest) (string, string, error) {
|
||||
return s.createMarkdownFile(req.Tool, req.Skill, "resources", req.FileName, defaultResourceMarkdown, "资源")
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetResource(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "resources", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveResource(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "resources", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateReference(_ context.Context, req request.SkillReferenceCreateRequest) (string, string, error) {
|
||||
return s.createMarkdownFile(req.Tool, req.Skill, "references", req.FileName, defaultReferenceMarkdown, "参考")
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetReference(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "references", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveReference(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "references", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateTemplate(_ context.Context, req request.SkillTemplateCreateRequest) (string, string, error) {
|
||||
return s.createMarkdownFile(req.Tool, req.Skill, "templates", req.FileName, defaultTemplateMarkdown, "模板")
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetTemplate(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "templates", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveTemplate(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "templates", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetGlobalConstraint(_ context.Context, tool string) (string, bool, error) {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
filePath := filepath.Join(skillsDir, globalConstraintFileName)
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return defaultGlobalConstraintMarkdown, false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
return string(content), true, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.SkillGlobalConstraintSaveRequest) error {
|
||||
if strings.TrimSpace(req.Tool) == "" {
|
||||
return errors.New("工具类型不能为空")
|
||||
}
|
||||
writeConstraint := func(tool, content string) error {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := filepath.Join(skillsDir, globalConstraintFileName)
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, []byte(content), 0644)
|
||||
}
|
||||
if err := writeConstraint(req.Tool, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(req.SyncTools) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, tool := range req.SyncTools {
|
||||
if tool == "" || tool == req.Tool {
|
||||
continue
|
||||
}
|
||||
if err := writeConstraint(tool, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) toolSkillsDir(tool string) (string, error) {
|
||||
toolDir, ok := skillToolDirs[tool]
|
||||
if !ok {
|
||||
return "", errors.New("工具类型不支持")
|
||||
}
|
||||
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
|
||||
if root == "" {
|
||||
root = "."
|
||||
}
|
||||
skillsDir := filepath.Join(root, toolDir, "skills")
|
||||
if err := os.MkdirAll(skillsDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return skillsDir, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) skillDir(tool, skill string) (string, error) {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(skillsDir, skill), nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) ensureSkillDir(tool, skill string) (string, error) {
|
||||
if !isSafeName(skill) {
|
||||
return "", errors.New("技能名称不合法")
|
||||
}
|
||||
skillDir, err := s.skillDir(tool, skill)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(skillDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return skillDir, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) createMarkdownFile(tool, skill, subDir, fileName, defaultContent, label string) (string, string, error) {
|
||||
if !isSafeName(skill) {
|
||||
return "", "", errors.New("技能名称不合法")
|
||||
}
|
||||
cleanName, err := buildResourceFileName(fileName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(tool, skill)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, subDir, cleanName)
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
if label == "" {
|
||||
label = "文件"
|
||||
}
|
||||
return "", "", fmt.Errorf("%s已存在", label)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
content := defaultContent
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return cleanName, content, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) readSkillFile(tool, skill, subDir, fileName string) (string, error) {
|
||||
if !isSafeName(skill) {
|
||||
return "", errors.New("技能名称不合法")
|
||||
}
|
||||
if !isSafeFileName(fileName) {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
skillDir, err := s.skillDir(tool, skill)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, subDir, fileName)
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) writeSkillFile(tool, skill, subDir, fileName, content string) error {
|
||||
if !isSafeName(skill) {
|
||||
return errors.New("技能名称不合法")
|
||||
}
|
||||
if !isSafeFileName(fileName) {
|
||||
return errors.New("文件名不合法")
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(tool, skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, subDir, fileName)
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
func parseSkillContent(content string) (system.SkillMeta, string, error) {
|
||||
clean := strings.TrimPrefix(content, "\ufeff")
|
||||
lines := strings.Split(clean, "\n")
|
||||
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
||||
return system.SkillMeta{}, clean, nil
|
||||
}
|
||||
end := -1
|
||||
for i := 1; i < len(lines); i++ {
|
||||
if strings.TrimSpace(lines[i]) == "---" {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if end == -1 {
|
||||
return system.SkillMeta{}, clean, nil
|
||||
}
|
||||
yamlText := strings.Join(lines[1:end], "\n")
|
||||
body := strings.Join(lines[end+1:], "\n")
|
||||
var meta system.SkillMeta
|
||||
if err := yaml.Unmarshal([]byte(yamlText), &meta); err != nil {
|
||||
return system.SkillMeta{}, body, err
|
||||
}
|
||||
return meta, body, nil
|
||||
}
|
||||
|
||||
func buildSkillContent(meta system.SkillMeta, markdown string) (string, error) {
|
||||
if meta.Name == "" {
|
||||
return "", errors.New("name不能为空")
|
||||
}
|
||||
data, err := yaml.Marshal(meta)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
yamlText := strings.TrimRight(string(data), "\n")
|
||||
body := strings.TrimLeft(markdown, "\n")
|
||||
if body != "" {
|
||||
body = body + "\n"
|
||||
}
|
||||
return fmt.Sprintf("---\n%s\n---\n%s", yamlText, body), nil
|
||||
}
|
||||
|
||||
func listFiles(dir string) []string {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
files := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Type().IsRegular() {
|
||||
files = append(files, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(files)
|
||||
return files
|
||||
}
|
||||
|
||||
func isSafeName(name string) bool {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
return false
|
||||
}
|
||||
return name == filepath.Base(name)
|
||||
}
|
||||
|
||||
func isSafeFileName(name string) bool {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
return false
|
||||
}
|
||||
return name == filepath.Base(name)
|
||||
}
|
||||
|
||||
func buildScriptFileName(fileName, scriptType string) (string, string, error) {
|
||||
clean := strings.TrimSpace(fileName)
|
||||
if clean == "" {
|
||||
return "", "", errors.New("文件名不能为空")
|
||||
}
|
||||
if !isSafeFileName(clean) {
|
||||
return "", "", errors.New("文件名不合法")
|
||||
}
|
||||
base := strings.TrimSuffix(clean, filepath.Ext(clean))
|
||||
if base == "" {
|
||||
return "", "", errors.New("文件名不合法")
|
||||
}
|
||||
|
||||
switch strings.ToLower(scriptType) {
|
||||
case "py", "python":
|
||||
return base + ".py", "python", nil
|
||||
case "js", "javascript", "script":
|
||||
return base + ".js", "javascript", nil
|
||||
case "sh", "shell", "bash":
|
||||
return base + ".sh", "sh", nil
|
||||
default:
|
||||
return "", "", errors.New("脚本类型不支持")
|
||||
}
|
||||
}
|
||||
|
||||
func buildResourceFileName(fileName string) (string, error) {
|
||||
clean := strings.TrimSpace(fileName)
|
||||
if clean == "" {
|
||||
return "", errors.New("文件名不能为空")
|
||||
}
|
||||
if !isSafeFileName(clean) {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
base := strings.TrimSuffix(clean, filepath.Ext(clean))
|
||||
if base == "" {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
return base + ".md", nil
|
||||
}
|
||||
|
||||
func scriptTemplate(lang string) string {
|
||||
switch lang {
|
||||
case "python":
|
||||
return "# -*- coding: utf-8 -*-\n# TODO: 在这里实现脚本逻辑\n"
|
||||
case "javascript":
|
||||
return "// TODO: 在这里实现脚本逻辑\n"
|
||||
case "sh":
|
||||
return "#!/usr/bin/env bash\nset -euo pipefail\n\n# TODO: 在这里实现脚本逻辑\n"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func copySkillDir(src, dst string) error {
|
||||
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
target := filepath.Join(dst, rel)
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, os.ModePerm)
|
||||
}
|
||||
if !d.Type().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(target, data, 0644)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user