shell: support Windows cmd /C; normalize date/time; allow all commands; add tests
This commit is contained in:
@@ -74,10 +74,10 @@ cluster_count: %d
|
||||
- 现有技能未命中,或命中后无法完成。
|
||||
|
||||
## 建议工具
|
||||
- 优先使用现有工具:`+"`shell`"+`、`+"`file`"+`。
|
||||
- 优先使用现有工具:`+"`shell`"+`、`+"`file`"+`、`+"`web_search`"+`。
|
||||
- 若能力不足,需要创建新工具时:
|
||||
1. 在 `+"`internal/tools/<tool_name>/`"+` 下生成 Go 代码;
|
||||
2. 在 `+"`cmd/bot/main.go`"+` 或 toolhost 注册逻辑中完成注册;
|
||||
1. 在 `+"`tools/<tool_name>/`"+` 下生成 Go 代码(实现 Name/Description/Call 接口);
|
||||
2. 在 `+"`internal/toolhost/runtime.go`"+` 中注册新工具;
|
||||
3. 生成/补充 `+"`*_test.go`"+`;
|
||||
4. 调用 `+"`go test ./...`"+` 验证。
|
||||
|
||||
@@ -87,7 +87,7 @@ cluster_count: %d
|
||||
3. 工具调用前先最小化探测范围。
|
||||
4. 工具失败时输出原因与下一步建议。
|
||||
5. 若缺少 skill:使用 `+"`file`"+` 与 `+"`shell`"+` 创建新的 `+"`skills/<skill_name>/skill.md`"+`。
|
||||
6. 若缺少 tool:生成工具代码与测试后执行 `+"`go test ./...`"+`。
|
||||
6. 若缺少 tool:在 `+"`tools/<tool_name>/`"+` 下生成工具代码与测试后执行 `+"`go test ./...`"+`。
|
||||
|
||||
## 输出规范
|
||||
- 结论:一句话给出当前阶段结论。
|
||||
|
||||
@@ -14,6 +14,21 @@ type Skill struct {
|
||||
Source string
|
||||
}
|
||||
|
||||
type SkillSummary struct {
|
||||
DirName string
|
||||
Name string
|
||||
Description string
|
||||
Source string
|
||||
}
|
||||
|
||||
type scannedSkill struct {
|
||||
dirName string
|
||||
name string
|
||||
description string
|
||||
content string
|
||||
source string
|
||||
}
|
||||
|
||||
func LoadSoul(path string) (string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -27,6 +42,39 @@ func LoadSoul(path string) (string, error) {
|
||||
}
|
||||
|
||||
func LoadSkillSet(dir string) ([]Skill, error) {
|
||||
scanned, err := scanSkills(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]Skill, 0, len(scanned))
|
||||
for _, s := range scanned {
|
||||
out = append(out, Skill{
|
||||
Name: s.name,
|
||||
Content: s.content,
|
||||
Source: s.source,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func LoadSkillSummaries(dir string) ([]SkillSummary, error) {
|
||||
scanned, err := scanSkills(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]SkillSummary, 0, len(scanned))
|
||||
for _, s := range scanned {
|
||||
out = append(out, SkillSummary{
|
||||
DirName: s.dirName,
|
||||
Name: s.name,
|
||||
Description: s.description,
|
||||
Source: s.source,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func scanSkills(dir string) ([]scannedSkill, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read skills dir failed: %w", err)
|
||||
@@ -40,7 +88,7 @@ func LoadSkillSet(dir string) ([]Skill, error) {
|
||||
}
|
||||
sort.Strings(skillDirs)
|
||||
|
||||
out := make([]Skill, 0, len(skillDirs))
|
||||
out := make([]scannedSkill, 0, len(skillDirs))
|
||||
for _, skillDir := range skillDirs {
|
||||
file := filepath.Join(dir, skillDir, "skill.md")
|
||||
b, err := os.ReadFile(file)
|
||||
@@ -54,12 +102,16 @@ func LoadSkillSet(dir string) ([]Skill, error) {
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
name := extractSkillName(skillDir, content)
|
||||
out = append(out, Skill{
|
||||
Name: name,
|
||||
Content: content,
|
||||
Source: file,
|
||||
name, description := parseSkillNameDescription(skillDir, content)
|
||||
if strings.TrimSpace(name) == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, scannedSkill{
|
||||
dirName: skillDir,
|
||||
name: name,
|
||||
description: description,
|
||||
content: content,
|
||||
source: file,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -88,3 +140,74 @@ func extractSkillName(fileName, markdown string) string {
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func parseSkillNameDescription(fileName, markdown string) (string, string) {
|
||||
name := ""
|
||||
description := ""
|
||||
fm := parseFrontMatter(markdown)
|
||||
if v, ok := fm["name"]; ok {
|
||||
name = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := fm["description"]; ok {
|
||||
description = strings.TrimSpace(v)
|
||||
}
|
||||
if name == "" {
|
||||
name = extractSkillName(fileName, markdown)
|
||||
}
|
||||
if description == "" {
|
||||
description = extractSkillDescription(markdown)
|
||||
}
|
||||
return name, description
|
||||
}
|
||||
|
||||
func parseFrontMatter(markdown string) map[string]string {
|
||||
lines := strings.Split(markdown, "\n")
|
||||
out := map[string]string{}
|
||||
if len(lines) < 3 {
|
||||
return out
|
||||
}
|
||||
if strings.TrimSpace(lines[0]) != "---" {
|
||||
return out
|
||||
}
|
||||
for i := 1; i < len(lines); i++ {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "---" {
|
||||
break
|
||||
}
|
||||
idx := strings.Index(line, ":")
|
||||
if idx <= 0 {
|
||||
continue
|
||||
}
|
||||
k := strings.ToLower(strings.TrimSpace(line[:idx]))
|
||||
v := strings.TrimSpace(line[idx+1:])
|
||||
v = strings.Trim(v, "\"'")
|
||||
if k != "" && v != "" {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extractSkillDescription(markdown string) string {
|
||||
lines := strings.Split(markdown, "\n")
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "---") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, ":") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
left := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
if left == "name" || left == "description" || left == "source" || left == "generated_at" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(line) > 200 {
|
||||
line = line[:200]
|
||||
}
|
||||
return line
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user