feat: implement dynamic changelog loading from API
- Add GET /api/changelog endpoint to fetch changelog from CHANGELOG.md - Create service/changelog.go to parse CHANGELOG.md markdown file - Add handler/changelog.go to handle changelog requests - Update ChangelogDrawer component to fetch from API instead of hardcoded data - Export apiFetch from lib/api.ts for public use - Add changelog parser tests with 14 version entries verified
This commit is contained in:
125
server/service/changelog.go
Normal file
125
server/service/changelog.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChangelogEntry 变更日志条目
|
||||
type ChangelogEntry struct {
|
||||
Version string `json:"version"`
|
||||
Date string `json:"date"`
|
||||
Changes map[string][]string `json:"changes"`
|
||||
}
|
||||
|
||||
// ParseChangelog 解析 CHANGELOG.md 文件
|
||||
func ParseChangelog() ([]ChangelogEntry, error) {
|
||||
// 获取项目根目录
|
||||
rootDir := os.Getenv("PROJECT_ROOT")
|
||||
if rootDir == "" {
|
||||
// 如果未设置,使用相对路径推测
|
||||
rootDir = ".."
|
||||
}
|
||||
|
||||
changelogPath := filepath.Join(rootDir, "CHANGELOG.md")
|
||||
|
||||
file, err := os.Open(changelogPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开 CHANGELOG.md 失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var entries []ChangelogEntry
|
||||
var currentEntry *ChangelogEntry
|
||||
var currentCategory string
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// 匹配版本号行 ## [1.4.0] - 2026-03-23
|
||||
if strings.HasPrefix(line, "## [") && strings.Contains(line, "]") {
|
||||
// 保存前一个 entry
|
||||
if currentEntry != nil {
|
||||
entries = append(entries, *currentEntry)
|
||||
}
|
||||
|
||||
// 解析版本号和日期
|
||||
version, date := parseVersionLine(line)
|
||||
if version != "" && version != "Unreleased" {
|
||||
currentEntry = &ChangelogEntry{
|
||||
Version: version,
|
||||
Date: date,
|
||||
Changes: make(map[string][]string),
|
||||
}
|
||||
currentCategory = ""
|
||||
} else {
|
||||
currentEntry = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过 Unreleased 和其他非版本行
|
||||
if currentEntry == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配分类行 ### 新增、### 优化等
|
||||
if strings.HasPrefix(line, "### ") {
|
||||
currentCategory = strings.TrimPrefix(line, "### ")
|
||||
if currentEntry.Changes[currentCategory] == nil {
|
||||
currentEntry.Changes[currentCategory] = []string{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配项目行 - 项目描述
|
||||
if strings.HasPrefix(line, "- ") && currentCategory != "" {
|
||||
item := strings.TrimPrefix(line, "- ")
|
||||
// 移除加粗标记和链接等
|
||||
item = cleanItem(item)
|
||||
currentEntry.Changes[currentCategory] = append(currentEntry.Changes[currentCategory], item)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个 entry
|
||||
if currentEntry != nil {
|
||||
entries = append(entries, *currentEntry)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("扫描文件失败: %w", err)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// parseVersionLine 解析版本行 ## [1.4.0] - 2026-03-23
|
||||
func parseVersionLine(line string) (version, date string) {
|
||||
// 提取版本号
|
||||
startIdx := strings.Index(line, "[")
|
||||
endIdx := strings.Index(line, "]")
|
||||
if startIdx >= 0 && endIdx > startIdx {
|
||||
version = line[startIdx+1 : endIdx]
|
||||
}
|
||||
|
||||
// 提取日期
|
||||
dateStartIdx := strings.LastIndex(line, "- ") + 2
|
||||
if dateStartIdx > 1 {
|
||||
date = strings.TrimSpace(line[dateStartIdx:])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// cleanItem 清理项目描述(移除加粗标记等)
|
||||
func cleanItem(item string) string {
|
||||
// 移除加粗标记 **text**
|
||||
item = strings.ReplaceAll(item, "**", "")
|
||||
// 移除代码标记 `text`
|
||||
item = strings.ReplaceAll(item, "`", "")
|
||||
return strings.TrimSpace(item)
|
||||
}
|
||||
38
server/service/changelog_test.go
Normal file
38
server/service/changelog_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseChangelog(t *testing.T) {
|
||||
// 设置项目根目录
|
||||
os.Setenv("PROJECT_ROOT", "../..")
|
||||
|
||||
changelog, err := ParseChangelog()
|
||||
if err != nil {
|
||||
t.Fatalf("ParseChangelog failed: %v", err)
|
||||
}
|
||||
|
||||
if len(changelog) == 0 {
|
||||
t.Fatal("No changelog entries parsed")
|
||||
}
|
||||
|
||||
// 验证第一个条目(应该是 1.4.0)
|
||||
firstEntry := changelog[0]
|
||||
t.Logf("First entry: v%s - %s", firstEntry.Version, firstEntry.Date)
|
||||
|
||||
if firstEntry.Version != "1.4.0" {
|
||||
t.Errorf("Expected first version to be 1.4.0, got %s", firstEntry.Version)
|
||||
}
|
||||
|
||||
if len(firstEntry.Changes) == 0 {
|
||||
t.Error("First entry has no changes")
|
||||
}
|
||||
|
||||
// 打印所有版本
|
||||
t.Logf("Total versions: %d", len(changelog))
|
||||
for _, entry := range changelog {
|
||||
t.Logf(" - v%s: %d categories", entry.Version, len(entry.Changes))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user