package knowledge import ( "fmt" "os" "path/filepath" "strings" "time" "laodingbot/internal/memory" ) func GenerateSkillDraft(cluster memory.CapabilityGapCluster, draftRoot string) (string, bool, error) { draftRoot = strings.TrimSpace(draftRoot) if draftRoot == "" { draftRoot = "./skills" } if err := os.MkdirAll(draftRoot, 0o755); err != nil { return "", false, err } skillDirName := "auto_" + slugFromIntent(cluster.IntentKey) if skillDirName == "" { skillDirName = "auto_gap_skill" } dir := filepath.Join(draftRoot, skillDirName) file := filepath.Join(dir, "skill.md") if _, err := os.Stat(file); err == nil { return file, false, nil } if err := os.MkdirAll(dir, 0o755); err != nil { return "", false, err } skillTitle := titleFromIntent(cluster.SampleIntent) if skillTitle == "" { skillTitle = "能力缺口补全技能" } content := buildDraftMarkdown(skillTitle, cluster) if err := os.WriteFile(file, []byte(content), 0o644); err != nil { return "", false, err } return file, true, nil } func buildDraftMarkdown(skillTitle string, cluster memory.CapabilityGapCluster) string { createdAt := time.Now().Format(time.RFC3339) return strings.TrimSpace(fmt.Sprintf(`--- name: %s description: 由 capability_gap 自动生成并用于自动补全缺失能力。 source: capability_gap generated_at: %s cluster_intent_key: %s cluster_reason: %s cluster_count: %d --- # Skill: %s ## 背景 - 该技能由系统根据高频能力缺口自动生成并已纳入技能目录。 - 最近高频缺口聚类:`+"`%s`"+` - 缺口原因:`+"`%s`"+` - 出现次数:`+"`%d`"+` ## 目标能力 - 明确该类问题应如何判断是否需要调用工具。 - 约束输入输出,避免泛化过度。 - 在失败时提供可操作回退路径。 ## 建议触发信号 - 用户提问与下述意图高度相关:`+"`%s`"+` - 现有技能未命中,或命中后无法完成。 ## 建议工具 - 优先使用现有工具:`+"`shell`"+`、`+"`file`"+`。 - 若能力不足,需要创建新工具时: 1. 在 `+"`internal/tools//`"+` 下生成 Go 代码; 2. 在 `+"`cmd/bot/main.go`"+` 或 toolhost 注册逻辑中完成注册; 3. 生成/补充 `+"`*_test.go`"+`; 4. 调用 `+"`go test ./...`"+` 验证。 ## ReAct 指南 1. 先确认用户目标和输入约束。 2. 判断是否可直接回答;若不行,再选择工具。 3. 工具调用前先最小化探测范围。 4. 工具失败时输出原因与下一步建议。 5. 若缺少 skill:使用 `+"`file`"+` 与 `+"`shell`"+` 创建新的 `+"`skills//skill.md`"+`。 6. 若缺少 tool:生成工具代码与测试后执行 `+"`go test ./...`"+`。 ## 输出规范 - 结论:一句话给出当前阶段结论。 - 依据:列出关键观察与证据。 - 限制:说明当前不确定性。 - 下一步:给用户可执行动作。 `, skillTitle, createdAt, cluster.IntentKey, cluster.Reason, cluster.Count, skillTitle, cluster.IntentKey, cluster.Reason, cluster.Count, cluster.SampleIntent)) } func slugFromIntent(intent string) string { intent = strings.TrimSpace(strings.ToLower(intent)) if intent == "" { return "" } b := strings.Builder{} lastDash := false for _, r := range intent { isAlphaNum := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') isCJK := r >= 0x4e00 && r <= 0x9fff if isAlphaNum || isCJK { b.WriteRune(r) lastDash = false continue } if !lastDash { b.WriteRune('-') lastDash = true } } out := strings.Trim(b.String(), "-") if out == "" { return "" } runes := []rune(out) if len(runes) > 48 { out = string(runes[:48]) } return out } func titleFromIntent(intent string) string { intent = strings.TrimSpace(intent) if intent == "" { return "" } runes := []rune(intent) if len(runes) > 32 { intent = string(runes[:32]) } return intent }