返回 DeepSeek-Reasonix
release_notes.go
根目录 / internal / productdocs / release_notes.go
1 package productdocs
2
3 import (
4 "encoding/json"
5 "fmt"
6 "strings"
7 )
8
9 type localizedText struct {
10 English string `json:"en"`
11 Chinese string `json:"zh"`
12 }
13
14 type releaseItem struct {
15 Kind string `json:"kind"`
16 Level string `json:"level"`
17 Title localizedText `json:"title"`
18 Body localizedText `json:"body"`
19 Href string `json:"href"`
20 Refs []int `json:"refs"`
21 }
22
23 type releaseRecord struct {
24 Version string `json:"version"`
25 Date string `json:"date"`
26 Channel string `json:"channel"`
27 Status string `json:"status"`
28 Title localizedText `json:"title"`
29 Summary localizedText `json:"summary"`
30 Surfaces []string `json:"surfaces"`
31 Guides []releaseItem `json:"guides"`
32 Highlights []releaseItem `json:"highlights"`
33 Upgrade []releaseItem `json:"upgrade"`
34 Risks []releaseItem `json:"risks"`
35 Contributors []string `json:"contributors"`
36 Changes struct {
37 New []releaseItem `json:"new"`
38 Improved []releaseItem `json:"improved"`
39 Fixed []releaseItem `json:"fixed"`
40 } `json:"changes"`
41 }
42
43 type renderedReleaseDocument struct {
44 path string
45 source string
46 locale string
47 version string
48 content string
49 }
50
51 func renderReleaseDocuments(data []byte) ([]renderedReleaseDocument, int, error) {
52 var catalog struct {
53 Releases []releaseRecord `json:"releases"`
54 }
55 if err := json.Unmarshal(data, &catalog); err != nil {
56 return nil, 0, fmt.Errorf("decode release-notes/releases.json: %w", err)
57 }
58 if len(catalog.Releases) == 0 {
59 return nil, 0, fmt.Errorf("release-notes/releases.json contains no releases")
60 }
61 seen := make(map[string]struct{}, len(catalog.Releases))
62 documents := make([]renderedReleaseDocument, 0, len(catalog.Releases)*2)
63 for _, release := range catalog.Releases {
64 if !validReleaseVersion(release.Version) {
65 return nil, 0, fmt.Errorf("release-notes/releases.json has invalid version %q", release.Version)
66 }
67 if _, duplicate := seen[release.Version]; duplicate {
68 return nil, 0, fmt.Errorf("release-notes/releases.json has duplicate version %q", release.Version)
69 }
70 seen[release.Version] = struct{}{}
71 for _, locale := range []string{"en", "zh-CN"} {
72 suffix := ".md"
73 if locale == "zh-CN" {
74 suffix = ".zh-CN.md"
75 }
76 documents = append(documents, renderedReleaseDocument{
77 path: "changelog/v" + release.Version + suffix,
78 source: "release-notes/releases.json#v" + release.Version,
79 locale: locale,
80 version: release.Version,
81 content: renderReleaseMarkdown(release, locale),
82 })
83 }
84 }
85 return documents, len(catalog.Releases), nil
86 }
87
88 func validReleaseVersion(version string) bool {
89 if version == "" || strings.Contains(version, "..") {
90 return false
91 }
92 for _, r := range version {
93 if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
94 continue
95 }
96 return false
97 }
98 return true
99 }
100
101 func renderReleaseMarkdown(release releaseRecord, locale string) string {
102 zh := locale == "zh-CN"
103 localize := func(value localizedText) string {
104 if zh {
105 return strings.TrimSpace(value.Chinese)
106 }
107 return strings.TrimSpace(value.English)
108 }
109 heading := func(en, cn string) string {
110 if zh {
111 return cn
112 }
113 return en
114 }
115 var b strings.Builder
116 fmt.Fprintf(&b, "# Reasonix v%s — %s\n\n", release.Version, localize(release.Title))
117 if zh {
118 fmt.Fprintf(&b, "发布日期:%s\n\n发布渠道:%s\n\n状态:%s\n", release.Date, release.Channel, release.Status)
119 } else {
120 fmt.Fprintf(&b, "Release date: %s\n\nChannel: %s\n\nStatus: %s\n", release.Date, release.Channel, release.Status)
121 }
122 if len(release.Surfaces) > 0 {
123 fmt.Fprintf(&b, "\n%s: %s\n", heading("Surfaces", "涉及产品面"), strings.Join(release.Surfaces, ", "))
124 }
125
126 fmt.Fprintf(&b, "\n## %s\n\n%s\n", heading("Summary", "摘要"), localize(release.Summary))
127 renderReleaseItems(&b, heading("Highlights", "亮点"), release.Highlights, localize, heading)
128 if len(release.Changes.New)+len(release.Changes.Improved)+len(release.Changes.Fixed) > 0 {
129 fmt.Fprintf(&b, "\n## %s\n", heading("Changes", "变更"))
130 renderReleaseItemGroup(&b, heading("New", "新增"), release.Changes.New, localize, heading)
131 renderReleaseItemGroup(&b, heading("Improved", "改进"), release.Changes.Improved, localize, heading)
132 renderReleaseItemGroup(&b, heading("Fixed", "修复"), release.Changes.Fixed, localize, heading)
133 }
134 renderReleaseItems(&b, heading("Upgrade notes", "升级说明"), release.Upgrade, localize, heading)
135 renderReleaseItems(&b, heading("Known risks", "已知风险"), release.Risks, localize, heading)
136 renderReleaseItems(&b, heading("Related guides", "相关指南"), release.Guides, localize, heading)
137 if len(release.Contributors) > 0 {
138 fmt.Fprintf(&b, "\n## %s\n\n%s\n", heading("Contributors", "贡献者"), strings.Join(release.Contributors, ", "))
139 }
140 return strings.TrimSpace(b.String()) + "\n"
141 }
142
143 func renderReleaseItems(
144 b *strings.Builder,
145 title string,
146 items []releaseItem,
147 localize func(localizedText) string,
148 heading func(string, string) string,
149 ) {
150 if len(items) == 0 {
151 return
152 }
153 fmt.Fprintf(b, "\n## %s\n", title)
154 renderReleaseItemBullets(b, items, localize, heading)
155 }
156
157 func renderReleaseItemGroup(
158 b *strings.Builder,
159 title string,
160 items []releaseItem,
161 localize func(localizedText) string,
162 heading func(string, string) string,
163 ) {
164 if len(items) == 0 {
165 return
166 }
167 fmt.Fprintf(b, "\n### %s\n", title)
168 renderReleaseItemBullets(b, items, localize, heading)
169 }
170
171 func renderReleaseItemBullets(
172 b *strings.Builder,
173 items []releaseItem,
174 localize func(localizedText) string,
175 heading func(string, string) string,
176 ) {
177 for _, item := range items {
178 title := localize(item.Title)
179 body := localize(item.Body)
180 if title == "" && body == "" {
181 continue
182 }
183 fmt.Fprintf(b, "\n- **%s**", title)
184 if item.Kind != "" {
185 fmt.Fprintf(b, " [%s]", item.Kind)
186 } else if item.Level != "" {
187 fmt.Fprintf(b, " [%s]", item.Level)
188 }
189 if body != "" {
190 fmt.Fprintf(b, ": %s", body)
191 }
192 if item.Href != "" {
193 fmt.Fprintf(b, " [%s](%s)", heading("Open guide", "打开指南"), item.Href)
194 }
195 if len(item.Refs) > 0 {
196 refs := make([]string, 0, len(item.Refs))
197 for _, ref := range item.Refs {
198 refs = append(refs, fmt.Sprintf("#%d", ref))
199 }
200 fmt.Fprintf(b, " (%s: %s)", heading("Refs", "关联"), strings.Join(refs, ", "))
201 }
202 b.WriteByte('\n')
203 }
204 }
205
205 lines GO