shell: support Windows cmd /C; normalize date/time; allow all commands; add tests

This commit is contained in:
2026-03-05 17:44:19 +08:00
parent 47b6059773
commit e2f806edb3
19 changed files with 989 additions and 350 deletions

View File

@@ -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 ./...`"+`
## 输出规范
- 结论:一句话给出当前阶段结论。

View File

@@ -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 ""
}