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()
|
}
|