diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef06b1e..3c42f0d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Derive version from tag shell: pwsh @@ -26,6 +28,67 @@ jobs: "TAG_NAME=$tag" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + - name: Generate release notes from commits + shell: pwsh + run: | + git fetch --force --tags + + $tag = $env:TAG_NAME + if ([string]::IsNullOrWhiteSpace($tag)) { + throw "TAG_NAME is empty" + } + + $repo = "${{ github.repository }}" + + $prev = "" + try { + $commit = (git rev-list -n 1 $tag).Trim() + if (-not [string]::IsNullOrWhiteSpace($commit)) { + $prev = (git describe --tags --abbrev=0 "$commit^" 2>$null).Trim() + } + } catch {} + + if ([string]::IsNullOrWhiteSpace($prev)) { + # Fallback: best-effort previous version tag by semver-ish sorting. + $prev = (git tag --list "v*" --sort=-v:refname | Where-Object { $_ -ne $tag } | Select-Object -First 1) + } + + $range = "" + if (-not [string]::IsNullOrWhiteSpace($prev)) { + $range = "$prev..$tag" + } + + $lines = @() + if (-not [string]::IsNullOrWhiteSpace($range)) { + $lines = @(git log --no-merges --pretty=format:"- %s (%h)" --reverse $range) + } else { + # First release tag / missing history: include a small recent window. + $lines = @(git log --no-merges --pretty=format:"- %s (%h)" --reverse -n 50) + } + + if (-not $lines -or $lines.Count -eq 0) { + $lines = @("- 修复了一些已知问题,提升了稳定性。") + } + + $max = 60 + if ($lines.Count -gt $max) { + $total = $lines.Count + $lines = @($lines | Select-Object -First $max) + $lines += "- ...(共 $total 条提交,更多请查看完整变更链接)" + } + + $body = @() + $body += "## 更新内容 ($tag)" + $body += "" + $body += $lines + + if (-not [string]::IsNullOrWhiteSpace($prev)) { + $body += "" + $body += "完整变更: https://github.com/$repo/compare/$prev...$tag" + } + + ($body -join "`n") | Out-File -FilePath release-notes.md -Encoding utf8 + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -71,7 +134,7 @@ jobs: with: tag_name: ${{ env.TAG_NAME }} name: ${{ env.TAG_NAME }} - generate_release_notes: true + body_path: release-notes.md files: | desktop/dist/*Setup*.exe desktop/dist/*Setup*.exe.blockmap diff --git a/desktop/src/main.cjs b/desktop/src/main.cjs index 2f2dace..66f283b 100644 --- a/desktop/src/main.cjs +++ b/desktop/src/main.cjs @@ -265,9 +265,108 @@ function setWindowProgressBar(value) { } catch {} } +function looksLikeHtml(input) { + if (!input) return false; + const s = String(input); + if (!s.includes("<") || !s.includes(">")) return false; + // Be conservative: only treat the note as HTML if it contains common tags we expect from GitHub-rendered bodies. + return /<(p|div|br|ul|ol|li|a|strong|em|tt|code|pre|h[1-6])\b/i.test(s); +} + +function htmlToPlainText(html) { + if (!html) return ""; + + let text = String(html); + + // Drop script/style blocks entirely. + text = text.replace(/]*>[\s\S]*?<\/script>/gi, ""); + text = text.replace(/]*>[\s\S]*?<\/style>/gi, ""); + + // Keep links readable after stripping tags. + text = text.replace( + /]*href=(["'])([^"']+)\1[^>]*>([\s\S]*?)<\/a>/gi, + (_m, _q, href, inner) => { + const innerText = String(inner).replace(/<[^>]*>/g, "").trim(); + const url = String(href || "").trim(); + if (!url) return innerText; + if (!innerText) return url; + return `${innerText} (${url})`; + } + ); + + // Preserve line breaks / list structure before stripping remaining tags. + text = text.replace(/<\s*br\s*\/?>/gi, "\n"); + text = text.replace(/<\/\s*(p|div|h1|h2|h3|h4|h5|h6)\s*>/gi, "\n"); + text = text.replace(/<\s*li[^>]*>/gi, "- "); + text = text.replace(/<\/\s*li\s*>/gi, "\n"); + text = text.replace(/<\/\s*(ul|ol)\s*>/gi, "\n"); + + // Strip remaining tags. + text = text.replace(/<[^>]*>/g, ""); + + // Decode the handful of entities we commonly see from GitHub-rendered HTML. + const named = { + nbsp: " ", + amp: "&", + lt: "<", + gt: ">", + quot: '"', + apos: "'", + "#39": "'", + }; + text = text.replace(/&([a-z0-9#]+);/gi, (m, name) => { + const key = String(name || "").toLowerCase(); + if (named[key] != null) return named[key]; + + // Numeric entities (decimal / hex). + const decMatch = key.match(/^#(\d+)$/); + if (decMatch) { + const n = Number(decMatch[1]); + if (Number.isFinite(n) && n >= 0 && n <= 0x10ffff) { + try { + return String.fromCodePoint(n); + } catch { + return m; + } + } + return m; + } + + const hexMatch = key.match(/^#x([0-9a-f]+)$/i); + if (hexMatch) { + const n = Number.parseInt(hexMatch[1], 16); + if (Number.isFinite(n) && n >= 0 && n <= 0x10ffff) { + try { + return String.fromCodePoint(n); + } catch { + return m; + } + } + return m; + } + + return m; + }); + + // Normalize whitespace/newlines. + text = text.replace(/\r\n/g, "\n"); + text = text.replace(/\n{3,}/g, "\n\n"); + return text.trim(); +} + function normalizeReleaseNotes(releaseNotes) { if (!releaseNotes) return ""; - if (typeof releaseNotes === "string") return releaseNotes; + + const normalizeText = (value) => { + if (value == null) return ""; + const raw = typeof value === "string" ? value : String(value); + const trimmed = raw.trim(); + if (!trimmed) return ""; + if (looksLikeHtml(trimmed)) return htmlToPlainText(trimmed); + return trimmed; + }; + + if (typeof releaseNotes === "string") return normalizeText(releaseNotes); if (Array.isArray(releaseNotes)) { const parts = []; for (const item of releaseNotes) { @@ -275,15 +374,17 @@ function normalizeReleaseNotes(releaseNotes) { const note = item?.note; const noteText = typeof note === "string" ? note : note != null ? JSON.stringify(note, null, 2) : ""; - const block = [version ? `v${version}` : "", noteText].filter(Boolean).join("\n"); + const block = [version ? `v${version}` : "", normalizeText(noteText)] + .filter(Boolean) + .join("\n"); if (block) parts.push(block); } return parts.join("\n\n"); } try { - return JSON.stringify(releaseNotes, null, 2); + return normalizeText(JSON.stringify(releaseNotes, null, 2)); } catch { - return String(releaseNotes); + return normalizeText(releaseNotes); } } diff --git a/frontend/components/DesktopUpdateDialog.vue b/frontend/components/DesktopUpdateDialog.vue index a30d512..c10b304 100644 --- a/frontend/components/DesktopUpdateDialog.vue +++ b/frontend/components/DesktopUpdateDialog.vue @@ -44,7 +44,7 @@ 剩余 {{ remainingText }}
-
+
@@ -69,7 +69,7 @@