返回 DeepSeek-Reasonix
release_notes.go
根目录 / internal / productdocs / release_notes.go
1 package productdocs
2
3 import (
4 "encoding/json"
5 "fmt"
6 "slices"
7 "strings"
8 )
9
10 type localizedText struct {
11 English string `json:"en"`
12 Chinese string `json:"zh"`
13 }
14
15 type releaseItem struct {
16 Kind string `json:"kind"`
17 Level string `json:"level"`
18 Title localizedText `json:"title"`
19 Body localizedText `json:"body"`
20 Href string `json:"href"`
21 Refs []int `json:"refs"`
22 Targets []string `json:"targets"`
23 }
24
25 type releaseRecord struct {
26 Version string `json:"version"`
27 TargetingVersion int `json:"targetingVersion"`
28 Date string `json:"date"`
29 Channel string `json:"channel"`
30 Status string `json:"status"`
31 Title localizedText `json:"title"`
32 Summary localizedText `json:"summary"`
33 Surfaces []string `json:"surfaces"`
34 Guides []releaseItem `json:"guides"`
35 Highlights []releaseItem `json:"highlights"`
36 Upgrade []releaseItem `json:"upgrade"`
37 Risks []releaseItem `json:"risks"`
38 Contributors []string `json:"contributors"`
39 Changes struct {
40 New []releaseItem `json:"new"`
41 Improved []releaseItem `json:"improved"`
42 Fixed []releaseItem `json:"fixed"`
43 } `json:"changes"`
44 }
45
46 type renderedReleaseDocument struct {
47 path string
48 source string
49 locale string
50 version string
51 content string
52 }
53
54 func renderReleaseDocuments(data []byte) ([]renderedReleaseDocument, int, error) {
55 var catalog struct {
56 Releases []releaseRecord `json:"releases"`
57 }
58 if err := json.Unmarshal(data, &catalog); err != nil {
59 return nil, 0, fmt.Errorf("decode release-notes/releases.json: %w", err)
60 }
61 if len(catalog.Releases) == 0 {
62 return nil, 0, fmt.Errorf("release-notes/releases.json contains no releases")
63 }
64 seen := make(map[string]struct{}, len(catalog.Releases))
65 documents := make([]renderedReleaseDocument, 0, len(catalog.Releases)*2)
66 for _, release := range catalog.Releases {
67 if !validReleaseVersion(release.Version) {
68 return nil, 0, fmt.Errorf("release-notes/releases.json has invalid version %q", release.Version)
69 }
70 if _, duplicate := seen[release.Version]; duplicate {
71 return nil, 0, fmt.Errorf("release-notes/releases.json has duplicate version %q", release.Version)
72 }
73 seen[release.Version] = struct{}{}
74 for _, locale := range []string{"en", "zh-CN"} {
75 suffix := ".md"
76 if locale == "zh-CN" {
77 suffix = ".zh-CN.md"
78 }
79 documents = append(documents, renderedReleaseDocument{
80 path: "changelog/v" + release.Version + suffix,
81 source: "release-notes/releases.json#v" + release.Version,
82 locale: locale,
83 version: release.Version,
84 content: renderReleaseMarkdown(release, locale),
85 })
86 }
87 }
88 return documents, len(catalog.Releases), nil
89 }
90
91 func validReleaseVersion(version string) bool {
92 if version == "" || strings.Contains(version, "..") {
93 return false
94 }
95 for _, r := range version {
96 if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
97 continue
98 }
99 return false
100 }
101 return true
102 }
103
104 func renderReleaseMarkdown(release releaseRecord, locale string) string {
105 zh := locale == "zh-CN"
106 localize := func(value localizedText) string {
107 if zh {
108 return strings.TrimSpace(value.Chinese)
109 }
110 return strings.TrimSpace(value.English)
111 }
112 heading := func(en, cn string) string {
113 if zh {
114 return cn
115 }
116 return en
117 }
118 var b strings.Builder
119 fmt.Fprintf(&b, "# Reasonix v%s — %s\n\n", release.Version, localize(release.Title))
120 if zh {
121 fmt.Fprintf(&b, "发布日期:%s\n\n发布渠道:%s\n\n状态:%s\n", release.Date, release.Channel, release.Status)
122 } else {
123 fmt.Fprintf(&b, "Release date: %s\n\nChannel: %s\n\nStatus: %s\n", release.Date, release.Channel, release.Status)
124 }
125 if len(release.Surfaces) > 0 {
126 fmt.Fprintf(&b, "\n%s: %s\n", heading("Surfaces", "涉及产品面"), strings.Join(release.Surfaces, ", "))
127 }
128
129 fmt.Fprintf(&b, "\n## %s\n\n%s\n", heading("Summary", "摘要"), localize(release.Summary))
130 if release.TargetingVersion == 1 {
131 fmt.Fprintf(
132 &b,
133 "\n**Desktop %d · CLI %d**\n",
134 releaseTargetCount(release, "desktop"),
135 releaseTargetCount(release, "cli"),
136 )
137 renderReleaseTargetSection(&b, release, "desktop", heading("Desktop updates", "桌面端更新"), localize, heading)
138 renderReleaseTargetSection(&b, release, "cli", heading("CLI updates", "CLI 端更新"), localize, heading)
139 renderOtherReleaseUpdates(&b, release, localize, heading)
140 } else {
141 renderReleaseItems(&b, heading("Highlights", "亮点"), release.Highlights, localize, heading)
142 if len(release.Changes.New)+len(release.Changes.Improved)+len(release.Changes.Fixed) > 0 {
143 fmt.Fprintf(&b, "\n## %s\n", heading("Changes", "变更"))
144 renderReleaseItemGroup(&b, heading("New", "新增"), release.Changes.New, localize, heading)
145 renderReleaseItemGroup(&b, heading("Improved", "改进"), release.Changes.Improved, localize, heading)
146 renderReleaseItemGroup(&b, heading("Fixed", "修复"), release.Changes.Fixed, localize, heading)
147 }
148 }
149 renderReleaseItems(&b, heading("Upgrade notes", "升级说明"), release.Upgrade, localize, heading)
150 renderReleaseItems(&b, heading("Known risks", "已知风险"), release.Risks, localize, heading)
151 renderReleaseItems(&b, heading("Related guides", "相关指南"), release.Guides, localize, heading)
152 if len(release.Contributors) > 0 {
153 fmt.Fprintf(&b, "\n## %s\n\n%s\n", heading("Contributors", "贡献者"), strings.Join(release.Contributors, ", "))
154 }
155 return strings.TrimSpace(b.String()) + "\n"
156 }
157
158 func releaseItems(release releaseRecord) []releaseItem {
159 items := make([]releaseItem, 0, len(release.Highlights)+len(release.Changes.New)+len(release.Changes.Improved)+len(release.Changes.Fixed)+len(release.Upgrade)+len(release.Risks))
160 items = append(items, release.Highlights...)
161 items = append(items, release.Changes.New...)
162 items = append(items, release.Changes.Improved...)
163 items = append(items, release.Changes.Fixed...)
164 items = append(items, release.Upgrade...)
165 items = append(items, release.Risks...)
166 return items
167 }
168
169 func releaseItemHasTarget(item releaseItem, target string) bool {
170 return slices.Contains(item.Targets, target)
171 }
172
173 func releaseTargetCount(release releaseRecord, target string) int {
174 count := 0
175 for _, item := range releaseItems(release) {
176 if releaseItemHasTarget(item, target) {
177 count++
178 }
179 }
180 return count
181 }
182
183 func releaseItemsForTarget(items []releaseItem, target string) []releaseItem {
184 filtered := make([]releaseItem, 0, len(items))
185 for _, item := range items {
186 if releaseItemHasTarget(item, target) {
187 filtered = append(filtered, item)
188 }
189 }
190 return filtered
191 }
192
193 func renderReleaseTargetSection(
194 b *strings.Builder,
195 release releaseRecord,
196 target string,
197 title string,
198 localize func(localizedText) string,
199 heading func(string, string) string,
200 ) {
201 fmt.Fprintf(b, "\n## %s\n", title)
202 highlights := releaseItemsForTarget(release.Highlights, target)
203 newItems := releaseItemsForTarget(release.Changes.New, target)
204 improved := releaseItemsForTarget(release.Changes.Improved, target)
205 fixed := releaseItemsForTarget(release.Changes.Fixed, target)
206 renderReleaseItemGroup(b, heading("Highlights", "重点内容"), highlights, localize, heading)
207 renderReleaseItemGroup(b, heading("New", "新增"), newItems, localize, heading)
208 renderReleaseItemGroup(b, heading("Improved", "改进"), improved, localize, heading)
209 renderReleaseItemGroup(b, heading("Fixed", "修复"), fixed, localize, heading)
210 if len(highlights)+len(newItems)+len(improved)+len(fixed) > 0 {
211 return
212 }
213 if target == "cli" && releaseTargetCount(release, target) == 0 {
214 fmt.Fprintf(b, "\n%s\n", heading(
215 "This release has no CLI features, improvements, or fixes; CLI users may skip it if preferred.",
216 "本版本没有 CLI 相关功能、改进或修复,CLI 用户可按需跳过。",
217 ))
218 return
219 }
220 fmt.Fprintf(b, "\n%s\n", heading(
221 "No feature or fix entries are listed here; see the upgrade and risk notes below.",
222 "此板块没有功能或修复条目;相关说明请查看下方升级提醒和风险提示。",
223 ))
224 }
225
226 func otherOnlyReleaseItems(items []releaseItem) []releaseItem {
227 filtered := make([]releaseItem, 0, len(items))
228 for _, item := range items {
229 if len(item.Targets) > 0 && !releaseItemHasTarget(item, "desktop") && !releaseItemHasTarget(item, "cli") {
230 filtered = append(filtered, item)
231 }
232 }
233 return filtered
234 }
235
236 func renderOtherReleaseUpdates(
237 b *strings.Builder,
238 release releaseRecord,
239 localize func(localizedText) string,
240 heading func(string, string) string,
241 ) {
242 items := otherOnlyReleaseItems(release.Highlights)
243 items = append(items, otherOnlyReleaseItems(release.Changes.New)...)
244 items = append(items, otherOnlyReleaseItems(release.Changes.Improved)...)
245 items = append(items, otherOnlyReleaseItems(release.Changes.Fixed)...)
246 if len(items) == 0 {
247 return
248 }
249 renderReleaseItems(b, heading("Other project updates", "其他项目更新"), items, localize, heading)
250 }
251
252 func renderReleaseItems(
253 b *strings.Builder,
254 title string,
255 items []releaseItem,
256 localize func(localizedText) string,
257 heading func(string, string) string,
258 ) {
259 if len(items) == 0 {
260 return
261 }
262 fmt.Fprintf(b, "\n## %s\n", title)
263 renderReleaseItemBullets(b, items, localize, heading)
264 }
265
266 func renderReleaseItemGroup(
267 b *strings.Builder,
268 title string,
269 items []releaseItem,
270 localize func(localizedText) string,
271 heading func(string, string) string,
272 ) {
273 if len(items) == 0 {
274 return
275 }
276 fmt.Fprintf(b, "\n### %s\n", title)
277 renderReleaseItemBullets(b, items, localize, heading)
278 }
279
280 func renderReleaseItemBullets(
281 b *strings.Builder,
282 items []releaseItem,
283 localize func(localizedText) string,
284 heading func(string, string) string,
285 ) {
286 for _, item := range items {
287 title := localize(item.Title)
288 body := localize(item.Body)
289 if title == "" && body == "" {
290 continue
291 }
292 fmt.Fprintf(b, "\n- **%s**", title)
293 if len(item.Targets) > 0 {
294 labels := make([]string, 0, len(item.Targets))
295 for _, target := range item.Targets {
296 labels = append(labels, releaseTargetLabel(target, heading))
297 }
298 fmt.Fprintf(b, " [%s]", strings.Join(labels, " · "))
299 }
300 if item.Kind != "" {
301 fmt.Fprintf(b, " [%s]", item.Kind)
302 } else if item.Level != "" {
303 fmt.Fprintf(b, " [%s]", item.Level)
304 }
305 if body != "" {
306 fmt.Fprintf(b, ": %s", body)
307 }
308 if item.Href != "" {
309 fmt.Fprintf(b, " [%s](%s)", heading("Open guide", "打开指南"), item.Href)
310 }
311 if len(item.Refs) > 0 {
312 refs := make([]string, 0, len(item.Refs))
313 for _, ref := range item.Refs {
314 refs = append(refs, fmt.Sprintf("#%d", ref))
315 }
316 fmt.Fprintf(b, " (%s: %s)", heading("Refs", "关联"), strings.Join(refs, ", "))
317 }
318 b.WriteByte('\n')
319 }
320 }
321
322 func releaseTargetLabel(target string, heading func(string, string) string) string {
323 switch target {
324 case "desktop":
325 return heading("Desktop", "桌面端")
326 case "cli":
327 return "CLI"
328 case "site":
329 return heading("Site", "网站")
330 case "service":
331 return heading("Service", "服务端")
332 default:
333 return target
334 }
335 }
336
336 lines GO