Merge remote-tracking branch 'origin/pr/544'

This commit is contained in:
fawney19
2026-05-23 18:41:52 +08:00
36 changed files with 4329 additions and 191 deletions
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest'
import { normalizeReleaseNotesForDisplay, trimReleaseNotesForDisplay } from '../releaseNotes'
describe('trimReleaseNotesForDisplay', () => {
it('keeps manual notes and removes GitHub auto-generated sections', () => {
const input = [
'### Features',
'- 新增在线更新',
'',
'### Bug Fixes',
'- 修复版本检查',
'',
'## What\'s Changed',
'* fix: something by @someone in https://github.com/example/repo/pull/1',
'',
'## New Contributors',
'* @someone made their first contribution',
'',
'**Full Changelog**: https://github.com/example/repo/compare/v1...v2',
].join('\n')
expect(trimReleaseNotesForDisplay(input)).toBe([
'### Features',
'- 新增在线更新',
'',
'### Bug Fixes',
'- 修复版本检查',
].join('\n'))
})
it('removes standalone full changelog lines', () => {
const input = '**Full Changelog**: https://github.com/example/repo/compare/v1...v2'
expect(trimReleaseNotesForDisplay(input)).toBe('')
})
it('returns original notes when there is no auto-generated footer', () => {
const input = [
'### Features',
'- 支持历史版本切换',
].join('\n')
expect(trimReleaseNotesForDisplay(input)).toBe(input)
})
})
describe('normalizeReleaseNotesForDisplay', () => {
it('keeps existing markdown structure intact', () => {
const input = [
'### Features',
'- 新增在线更新',
'',
'### Fixes',
'- 修复版本检查',
].join('\n')
expect(normalizeReleaseNotesForDisplay(input)).toBe(input)
})
it('converts plain multi-section notes into markdown sections', () => {
const input = [
'Features',
'新增在线更新弹窗',
'支持选择历史版本',
'',
'问题修复',
'修复下载进度显示不准',
'修复版本详情跳转错误',
].join('\n')
expect(normalizeReleaseNotesForDisplay(input)).toBe([
'### Features',
'- 新增在线更新弹窗',
'- 支持选择历史版本',
'',
'### 问题修复',
'- 修复下载进度显示不准',
'- 修复版本详情跳转错误',
].join('\n'))
})
it('does not over-normalize plain paragraph text', () => {
const input = '这次更新主要修复了在线更新流程中的代理问题,并优化了历史版本切换体验。'
expect(normalizeReleaseNotesForDisplay(input)).toBe(input)
})
})
@@ -10,6 +10,8 @@ function updateStatus(overrides: Partial<CheckUpdateResponse> = {}): CheckUpdate
current_version: '0.7.0-rc27',
latest_version: null,
has_update: false,
updatable: false,
update_blocker: null,
release_url: null,
release_notes: null,
published_at: null,
+144
View File
@@ -0,0 +1,144 @@
const AUTO_GENERATED_RELEASE_SECTION_PATTERNS = [
/^#{1,6}\s+what'?s changed\s*$/i,
/^#{1,6}\s+new contributors\s*$/i,
/^\*\*full changelog\*\*:/i,
]
const MARKDOWN_STRUCTURE_PATTERNS = [
/^#{1,6}\s+\S/,
/^\s*[-*+]\s+\S/,
/^\s*\d+\.\s+\S/,
/^\s*>\s+\S/,
/^\s*```/,
/^\s*---+\s*$/,
/^\s*\|(?:[^|]+\|)+\s*$/,
/`[^`]+`/,
/\[[^\]]+\]\([^)]+\)/,
]
const SENTENCE_END_PUNCTUATION = /[,。,.!?!?;;]$/
const SECTION_HEADING_SUFFIX = /[::]\s*$/
const URL_PATTERN = /https?:\/\/|www\./i
const BRACKET_PREFIX_PATTERN = /^[\[((【<]/
const SECTION_HEADING_TEXT_PATTERN = /[\u3400-\u9FFFA-Za-z]/
function isStructuredMarkdownLine(line: string): boolean {
return MARKDOWN_STRUCTURE_PATTERNS.some((pattern) => pattern.test(line))
}
function normalizeSectionHeading(line: string): string {
return line.replace(SECTION_HEADING_SUFFIX, '').trim()
}
function looksLikeSectionHeading(line: string): boolean {
const trimmed = normalizeSectionHeading(line)
if (!trimmed) return false
if (trimmed.length > 24) return false
if (URL_PATTERN.test(trimmed)) return false
if (BRACKET_PREFIX_PATTERN.test(trimmed)) return false
if (isStructuredMarkdownLine(trimmed)) return false
if (SENTENCE_END_PUNCTUATION.test(trimmed)) return false
if (!SECTION_HEADING_TEXT_PATTERN.test(trimmed)) return false
return true
}
function collectConfirmedHeadingIndexes(lines: string[]): Set<number> {
const candidates = lines
.map((line, index) => {
if (!looksLikeSectionHeading(line.trim())) return -1
if (index === 0) return index
return lines[index - 1].trim() === '' ? index : -1
})
.filter((index) => index !== -1)
const confirmed = new Set<number>()
for (let i = 0; i < candidates.length; i += 1) {
const current = candidates[i]
const next = i + 1 < candidates.length ? candidates[i + 1] : lines.length
for (let cursor = current + 1; cursor < next; cursor += 1) {
const content = lines[cursor].trim()
if (!content) continue
confirmed.add(current)
break
}
}
return confirmed
}
function collapseBlankLines(lines: string[]): string {
return lines
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
export function trimReleaseNotesForDisplay(notes: string | null | undefined): string {
if (!notes) return ''
const normalized = notes.replace(/\r\n?/g, '\n').trim()
if (!normalized) return ''
const lines = normalized.split('\n')
const cutoff = lines.findIndex((line) => {
const trimmed = line.trim()
return AUTO_GENERATED_RELEASE_SECTION_PATTERNS.some((pattern) => pattern.test(trimmed))
})
if (cutoff === -1) {
return normalized
}
return lines.slice(0, cutoff).join('\n').trim()
}
export function normalizeReleaseNotesForDisplay(notes: string | null | undefined): string {
const trimmed = trimReleaseNotesForDisplay(notes)
if (!trimmed) return ''
const lines = trimmed.split('\n')
if (lines.some((line) => isStructuredMarkdownLine(line.trim()))) {
return trimmed
}
const headingIndexes = collectConfirmedHeadingIndexes(lines)
if (headingIndexes.size < 2) {
return trimmed
}
const normalized: string[] = []
let insideSection = false
for (let index = 0; index < lines.length; index += 1) {
const trimmedLine = lines[index].trim()
if (!trimmedLine) {
if (normalized.length > 0 && normalized[normalized.length - 1] !== '') {
normalized.push('')
}
continue
}
if (headingIndexes.has(index)) {
if (normalized.length > 0 && normalized[normalized.length - 1] !== '') {
normalized.push('')
}
normalized.push(`### ${normalizeSectionHeading(trimmedLine)}`)
insideSection = true
continue
}
if (insideSection) {
normalized.push(`- ${trimmedLine.replace(/^[-*+•]\s*/, '')}`)
continue
}
normalized.push(trimmedLine)
}
return collapseBlankLines(normalized)
}
+2
View File
@@ -15,6 +15,8 @@ export function buildUpdateErrorStatus(
current_version: previousStatus?.current_version || '',
latest_version: null,
has_update: false,
updatable: false,
update_blocker: null,
release_url: null,
release_notes: null,
published_at: null,