package review import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "strings" "gopkg.in/yaml.v3" ) // DraftInfo 草稿基本信息 type DraftInfo struct { Path string `json:"path"` Title string `json:"title"` Source string `json:"source"` Tags []string `json:"tags"` } // MergeHint 合并指示 type MergeHint struct { Action string `json:"action"` Target string `json:"target"` Reason string `json:"reason"` Confidence string `json:"confidence"` } // TargetFAQ 目标FAQ信息 type TargetFAQ struct { Path string `json:"path"` Title string `json:"title"` Summary string `json:"summary"` // LLM 生成的3句话概要 } // ReviewPreview 审阅预览结果 type ReviewPreview struct { Draft DraftInfo `json:"draft"` DraftContent string `json:"draft_content"` // 草稿正文(去掉frontmatter) MergeHint MergeHint `json:"merge_hint"` TargetFAQ *TargetFAQ `json:"target_faq,omitempty"` } // expandPath 展开路径中的 ~ func expandPath(path string) string { if strings.HasPrefix(path, "~/") { homeDir, _ := os.UserHomeDir() return filepath.Join(homeDir, path[2:]) } return path } // extractMainContent 从 draft.md 中提取正文(去掉 frontmatter) func extractMainContent(content string) string { if !strings.HasPrefix(content, "---") { return content } // 找到第二个 --- endIdx := strings.Index(content[3:], "\n---") if endIdx == -1 { return content } // 返回 frontmatter 之后的内容 mainContent := content[endIdx+6:] return strings.TrimSpace(mainContent) } // ReadDraftAndMerge 读取草稿目录的 draft.md 和 merge.md func ReadDraftAndMerge(vaultPath, draftDir string) (*ReviewPreview, error) { // 展开路径中的 ~ vaultPath = expandPath(vaultPath) // 构建完整路径 fullDraftDir := draftDir if !filepath.IsAbs(draftDir) { fullDraftDir = filepath.Join(vaultPath, draftDir) } // 检查目录是否存在 if _, err := os.Stat(fullDraftDir); os.IsNotExist(err) { return nil, fmt.Errorf("目录不存在: %s", fullDraftDir) } // 读取 draft.md draftPath := filepath.Join(fullDraftDir, "draft.md") draftContent, err := os.ReadFile(draftPath) if err != nil { return nil, fmt.Errorf("读取 draft.md 失败: %w", err) } // 解析 draft.md frontmatter draftMeta := parseFrontmatter(string(draftContent)) draftInfo := DraftInfo{ Path: draftDir, Title: draftMeta["title"], Source: draftMeta["source"], Tags: parseTags(draftMeta["tags"]), } // 读取 merge.md mergePath := filepath.Join(fullDraftDir, "merge.md") mergeHint := MergeHint{ Action: "new", Confidence: "low", } if mergeContent, err := os.ReadFile(mergePath); err == nil { mergeHint = parseMergeMd(string(mergeContent)) } // 构建预览结果 preview := &ReviewPreview{ Draft: draftInfo, DraftContent: extractMainContent(string(draftContent)), MergeHint: mergeHint, } // 如果 action=merge,读取目标FAQ if mergeHint.Action == "merge" && mergeHint.Target != "" { targetPath := mergeHint.Target if !filepath.IsAbs(targetPath) { targetPath = filepath.Join(vaultPath, targetPath) } if targetContent, err := os.ReadFile(targetPath); err == nil { targetMeta := parseFrontmatter(string(targetContent)) preview.TargetFAQ = &TargetFAQ{ Path: mergeHint.Target, Title: targetMeta["title"], } } } return preview, nil } // parseFrontmatter 解析 YAML frontmatter func parseFrontmatter(content string) map[string]string { result := make(map[string]string) if !strings.HasPrefix(content, "---") { return result } // 找到第二个 --- endIdx := strings.Index(content[3:], "\n---") if endIdx == -1 { return result } yamlStr := content[3 : endIdx+3] var data map[string]interface{} if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { return result } // 转换为 string map for k, v := range data { switch val := v.(type) { case string: result[k] = val case []interface{}: // tags 可能是数组 var tags []string for _, t := range val { if s, ok := t.(string); ok { tags = append(tags, s) } } if len(tags) > 0 { result[k] = strings.Join(tags, ",") } } } return result } // parseTags 解析 tags 字符串为数组 func parseTags(tagsStr string) []string { if tagsStr == "" { return []string{} } tags := strings.Split(tagsStr, ",") result := make([]string, 0, len(tags)) for _, t := range tags { t = strings.TrimSpace(t) if t != "" { result = append(result, t) } } return result } // parseMergeMd 解析 merge.md 内容 func parseMergeMd(content string) MergeHint { result := MergeHint{ Action: "new", Confidence: "low", } // 解析 frontmatter 中的 confidence fmMatch := regexp.MustCompile(`(?s)^---\s*\n(.*?)\n---`).FindStringSubmatch(content) if len(fmMatch) > 1 { var fm map[string]interface{} if err := yaml.Unmarshal([]byte(fmMatch[1]), &fm); err == nil { if conf, ok := fm["confidence"].(string); ok { result.Confidence = conf } } } // 提取操作类型 if match := regexp.MustCompile(`\*\*操作类型\*\*:\s*(\w+)`).FindStringSubmatch(content); len(match) > 1 { result.Action = match[1] } // 提取目标 if match := regexp.MustCompile(`\*\*目标\*\*:[ \t]*(.+)`).FindStringSubmatch(content); len(match) > 1 { result.Target = strings.TrimSpace(match[1]) } // 提取理由 if match := regexp.MustCompile(`\*\*理由\*\*:[ \t]*(.+)`).FindStringSubmatch(content); len(match) > 1 { result.Reason = strings.TrimSpace(match[1]) } return result } // FormatPreviewJSON 格式化预览结果为 JSON func FormatPreviewJSON(preview *ReviewPreview) (string, error) { data, err := json.MarshalIndent(preview, "", " ") if err != nil { return "", err } return string(data), nil } // FormatPreviewTable 格式化预览结果为表格 func FormatPreviewTable(preview *ReviewPreview) string { var sb strings.Builder sb.WriteString("## 📋 审阅预览\n\n") // 草稿信息 sb.WriteString("### 草稿信息\n\n") sb.WriteString(fmt.Sprintf("**标题:** %s\n", preview.Draft.Title)) sb.WriteString(fmt.Sprintf("**路径:** `%s`\n", preview.Draft.Path)) if preview.Draft.Source != "" { sb.WriteString(fmt.Sprintf("**来源:** %s\n", preview.Draft.Source)) } if len(preview.Draft.Tags) > 0 { sb.WriteString(fmt.Sprintf("**标签:** %s\n", strings.Join(preview.Draft.Tags, ", "))) } sb.WriteString("\n") // 合并指示 sb.WriteString("### 合并指示\n\n") sb.WriteString(fmt.Sprintf("**操作类型:** %s\n", preview.MergeHint.Action)) if preview.MergeHint.Target != "" { sb.WriteString(fmt.Sprintf("**目标:** `%s`\n", preview.MergeHint.Target)) } if preview.MergeHint.Reason != "" { sb.WriteString(fmt.Sprintf("**理由:** %s\n", preview.MergeHint.Reason)) } sb.WriteString(fmt.Sprintf("**置信度:** %s\n\n", preview.MergeHint.Confidence)) // 目标FAQ概要 if preview.TargetFAQ != nil { sb.WriteString("### 目标FAQ概要\n\n") sb.WriteString(fmt.Sprintf("**标题:** %s\n", preview.TargetFAQ.Title)) sb.WriteString(fmt.Sprintf("**路径:** `%s`\n", preview.TargetFAQ.Path)) if preview.TargetFAQ.Summary != "" { sb.WriteString(fmt.Sprintf("**概要:**\n%s\n", preview.TargetFAQ.Summary)) } sb.WriteString("\n") } // 操作选项 sb.WriteString("---\n\n") sb.WriteString("**请确认:**\n") sb.WriteString("- A) 追加/合并到目标 FAQ\n") sb.WriteString("- B) 归档(保留历史但不合并)\n") sb.WriteString("- C) 删除(内容简单无复用价值)\n") sb.WriteString("- D) 保留跟进(更新提醒日期)\n") return sb.String() }