feat: 版本日志菜单 + CHANGELOG.md 自动同步到 changelog.json

- 新增 scripts/sync-changelog.js:解析 CHANGELOG.md 生成 changelog.json,
  自动过滤噪音条目(bump version、docs、refactor 等内部描述)
- safe-version-bump.js:version:patch 时自动调用 sync-changelog 并提交 changelog.json
- versions.html:简化为纯 changelog 展示页,去掉版本跳转和 CDN 链接,
  默认显示 30 个版本 + 加载更多按钮,展开详情 max-height 调至 5000px
- app-menu-items.tsx:新增「版本日志」菜单项(打开 versions.html),
  「版本更多」恢复跳转到 release.opentu.ai
- update-version.js:构建时检查 changelog.json 是否包含当前版本
- upload-and-deploy.js:部署时复制 versions.html 和 changelog.json 到 releases 根目录
- i18n.tsx:新增 menu.changelog 翻译 key
This commit is contained in:
gongchengtu committed 2026-02-26 15:01:02 +08:00
1 parent ef97460391
commit 51bb033edb
9 files changed
+2358 -342

No files matched your search

File diff suppressed because it is too large. Load diff
+188 -329
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenTu - 版本选择</title>
<title>OpenTu - 版本日志</title>
<style>
:root {
--primary-color: #f39c12;
@@ -13,6 +13,7 @@
--text-color: #333;
--text-secondary: #666;
--border-color: #e0e0e0;
--details-bg: #fafafa;
}
@media (prefers-color-scheme: dark) {
@@ -22,407 +23,265 @@
--text-color: #e0e0e0;
--text-secondary: #999;
--border-color: #444;
--details-bg: #242424;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.container { max-width: 800px; margin: 0 auto; }
header {
text-align: center;
margin-bottom: 30px;
}
header { text-align: center; margin-bottom: 30px; }
h1 {
font-size: 28px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
h1 .icon {
font-size: 32px;
}
.subtitle {
color: var(--text-secondary);
font-size: 14px;
}
.current-info {
background: var(--card-bg);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.current-info h2 {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.current-version {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
}
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
font-size: 28px; margin-bottom: 8px;
display: flex; align-items: center; justify-content: center; gap: 10px;
}
h1 .icon { font-size: 32px; }
.subtitle { color: var(--text-secondary); font-size: 14px; }
.loading { text-align: center; padding: 40px; color: var(--text-secondary); }
.loading .spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
width: 40px; height: 40px; border: 3px solid var(--border-color);
border-top-color: var(--primary-color); border-radius: 50%;
animation: spin 1s linear infinite; margin: 0 auto 15px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.error {
background: #fff0f0;
border: 1px solid #ffccc7;
color: #cf1322;
padding: 20px;
border-radius: 12px;
text-align: center;
background: #fff0f0; border: 1px solid #ffccc7; color: #cf1322;
padding: 20px; border-radius: 12px; text-align: center;
}
@media (prefers-color-scheme: dark) {
.error { background: #3a2020; border-color: #5a3030; color: #ff7875; }
}
.version-list {
display: flex;
flex-direction: column;
gap: 12px;
.version-group { margin-bottom: 24px; }
.group-title {
font-size: 18px; font-weight: 600; margin-bottom: 12px;
padding-bottom: 8px; border-bottom: 2px solid var(--primary-color);
color: var(--primary-color);
}
.version-list { display: flex; flex-direction: column; gap: 10px; }
.version-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.2s ease;
cursor: pointer;
text-decoration: none;
color: inherit;
background: var(--card-bg); border: 1px solid var(--border-color);
border-radius: 12px; transition: all 0.2s ease; overflow: hidden;
}
.version-card:hover {
border-color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.version-card.current {
border-color: var(--primary-color);
background: linear-gradient(
135deg,
rgba(243, 156, 18, 0.1) 0%,
rgba(230, 126, 34, 0.05) 100%
);
.version-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; cursor: pointer; user-select: none;
}
.version-info {
display: flex;
align-items: center;
gap: 12px;
}
.version-number {
font-size: 18px;
font-weight: 600;
}
.version-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.version-number { font-size: 17px; font-weight: 600; }
.version-date { font-size: 13px; color: var(--text-secondary); }
.version-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background: var(--primary-color);
color: white;
font-size: 11px; padding: 2px 8px; border-radius: 4px;
background: var(--primary-color); color: white;
}
.version-tag.major { background: #722ed1; }
.version-tag.minor { background: #1890ff; }
.version-highlights {
font-size: 13px; color: var(--text-secondary);
margin-top: 4px; line-height: 1.4;
}
.version-tag.latest {
background: #52c41a;
.version-right { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.expand-btn {
background: none; border: none; cursor: pointer; padding: 4px;
color: var(--text-secondary); transition: transform 0.2s ease;
font-size: 18px; line-height: 1;
}
.expand-btn.expanded { transform: rotate(180deg); }
.version-details {
max-height: 0; overflow: hidden; transition: max-height 0.3s ease;
border-top: none;
}
.version-details.open {
max-height: 5000px; border-top: 1px solid var(--border-color);
}
.version-details-inner { padding: 14px 20px; background: var(--details-bg); }
.change-category { margin-bottom: 12px; }
.change-category:last-child { margin-bottom: 0; }
.change-category-title {
font-size: 13px; font-weight: 600; margin-bottom: 6px;
display: flex; align-items: center; gap: 6px;
}
.change-category-title .emoji { font-size: 14px; }
.change-list {
list-style: none; padding: 0; margin: 0;
font-size: 13px; line-height: 1.8;
}
.change-list li { padding-left: 16px; position: relative; }
.change-list li::before {
content: '•'; position: absolute; left: 4px; color: var(--text-secondary);
}
.version-tag.current {
background: var(--primary-color);
}
.version-action {
color: var(--primary-color);
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
}
.version-action svg {
width: 16px;
height: 16px;
.load-more {
display: block; width: 100%; padding: 12px; margin-top: 16px;
background: var(--card-bg); border: 1px solid var(--border-color);
border-radius: 8px; color: var(--primary-color); font-size: 14px;
cursor: pointer; transition: all 0.2s;
}
.load-more:hover { border-color: var(--primary-color); background: rgba(243,156,18,0.05); }
.footer {
margin-top: 30px;
text-align: center;
color: var(--text-secondary);
font-size: 13px;
}
.footer a {
color: var(--primary-color);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.cdn-links {
margin-top: 20px;
padding: 15px;
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.cdn-links h3 {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 10px;
}
.cdn-link {
display: block;
padding: 8px 12px;
background: var(--bg-color);
border-radius: 6px;
margin-bottom: 8px;
font-family: monospace;
font-size: 13px;
word-break: break-all;
color: var(--text-color);
text-decoration: none;
}
.cdn-link:hover {
background: rgba(243, 156, 18, 0.1);
}
.cdn-link:last-child {
margin-bottom: 0;
margin-top: 30px; text-align: center; color: var(--text-secondary); font-size: 13px;
}
.footer a { color: var(--primary-color); text-decoration: none; }
.footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>
<span class="icon">🎨</span>
OpenTu - 版本选择
<span class="icon">📋</span>
OpenTu - 版本日志
</h1>
<p class="subtitle">选择要使用的版本</p>
<p class="subtitle">点击展开查看更新详情</p>
</header>
<div class="current-info">
<h2>当前版本</h2>
<div class="current-version" id="currentVersion">加载中...</div>
</div>
<div id="versionList" class="loading">
<div class="spinner"></div>
<p>正在加载版本列表...</p>
</div>
<div class="cdn-links">
<h3>CDN 访问链接</h3>
<a class="cdn-link" id="unpkgLink" href="#" target="_blank"
>unpkg: 加载中...</a
>
<a class="cdn-link" id="jsdelivrLink" href="#" target="_blank"
>jsdelivr: 加载中...</a
>
<p>正在加载版本日志...</p>
</div>
<footer class="footer">
<p>
<a href="https://github.com/ljquan/aitu" target="_blank">GitHub</a> ·
<a href="https://opentu.ai" target="_blank">官网</a> ·
<a href="https://release.opentu.ai" target="_blank">更多版本</a>
<a href="https://opentu.ai" target="_blank">官网</a>
</p>
</footer>
</div>
<script>
const PACKAGE_NAME = 'aitu-app';
const NPM_REGISTRY = 'https://registry.npmjs.org';
// 获取当前版本
function getCurrentVersion() {
// 从 URL 路径中提取版本号
const match = window.location.pathname.match(/@([^/]+)/);
if (match) {
return match[1];
function groupByMinor(versions) {
const groups = {};
for (const v of versions) {
const parts = v.version.split('.');
const key = parts[0] + '.' + parts[1];
if (!groups[key]) groups[key] = [];
groups[key].push(v);
}
// 从 meta 标签获取
const meta = document.querySelector('meta[name="app-version"]');
if (meta) {
return meta.getAttribute('content');
}
return null;
return groups;
}
// 获取当前 CDN 类型
function getCdnType() {
const hostname = window.location.hostname;
if (hostname.includes('unpkg.com')) return 'unpkg';
if (hostname.includes('jsdelivr.net')) return 'jsdelivr';
return 'unpkg'; // 默认
}
// 生成版本链接
function getVersionUrl(version, cdnType) {
if (cdnType === 'jsdelivr') {
return `https://cdn.jsdelivr.net/npm/${PACKAGE_NAME}@${version}/index.html`;
function renderChangeDetails(changelog) {
if (!changelog || !changelog.changes) return '';
const categories = [
{ key: 'features', emoji: '✨', label: '新功能' },
{ key: 'fixes', emoji: '🐛', label: '修复' },
{ key: 'improvements', emoji: '💡', label: '优化' },
];
let html = '';
for (const cat of categories) {
const items = changelog.changes[cat.key];
if (!items || items.length === 0) continue;
html += '<div class="change-category">' +
'<div class="change-category-title">' +
'<span class="emoji">' + cat.emoji + '</span>' + cat.label +
'</div><ul class="change-list">' +
items.map(function(item) { return '<li>' + item + '</li>'; }).join('') +
'</ul></div>';
}
return `https://unpkg.com/${PACKAGE_NAME}@${version}/index.html`;
return html;
}
// 获取版本列表
async function fetchVersions() {
try {
const response = await fetch(`${NPM_REGISTRY}/${PACKAGE_NAME}`);
if (!response.ok) {
throw new Error('Failed to fetch package info');
function toggleDetails(cardId) {
var details = document.getElementById('details-' + cardId);
var btn = document.getElementById('btn-' + cardId);
if (!details || !btn) return;
details.classList.toggle('open');
btn.classList.toggle('expanded');
}
var PAGE_SIZE = 30;
var allVersions = [];
function renderVersionList(versions) {
var container = document.getElementById('versionList');
var groups = groupByMinor(versions);
var isFirst = true;
var html = '';
for (var groupKey in groups) {
var groupVersions = groups[groupKey];
html += '<div class="version-group">' +
'<div class="group-title">v' + groupKey + '.x</div>' +
'<div class="version-list">';
for (var i = 0; i < groupVersions.length; i++) {
var entry = groupVersions[i];
var hasDetails = entry.changes && Object.values(entry.changes).some(function(a) { return a && a.length > 0; });
var cardId = entry.version.replace(/\./g, '-');
var tags = '';
if (entry.type === 'major') tags += '<span class="version-tag major">大版本</span>';
else if (entry.type === 'minor') tags += '<span class="version-tag minor">新版本</span>';
var dateStr = entry.date || '';
var highlights = entry.highlights || '';
var detailsHtml = hasDetails ? renderChangeDetails(entry) : '';
var defaultOpen = isFirst && hasDetails;
html += '<div class="version-card">' +
'<div class="version-header"' + (hasDetails ? ' onclick="toggleDetails(\'' + cardId + '\')"' : '') + '>' +
'<div>' +
'<div class="version-meta">' +
'<span class="version-number">v' + entry.version + '</span>' +
tags +
(dateStr ? '<span class="version-date">' + dateStr + '</span>' : '') +
'</div>' +
(highlights ? '<div class="version-highlights">' + highlights + '</div>' : '') +
'</div>' +
'<div class="version-right">' +
(hasDetails ? '<button class="expand-btn' + (defaultOpen ? ' expanded' : '') + '" id="btn-' + cardId + '" onclick="event.stopPropagation();toggleDetails(\'' + cardId + '\')">▾</button>' : '') +
'</div>' +
'</div>' +
(hasDetails ? '<div class="version-details' + (defaultOpen ? ' open' : '') + '" id="details-' + cardId + '"><div class="version-details-inner">' + detailsHtml + '</div></div>' : '') +
'</div>';
isFirst = false;
}
const data = await response.json();
return {
versions: Object.keys(data.versions).reverse(),
latest:
data['dist-tags']?.latest || Object.keys(data.versions).pop(),
};
} catch (error) {
console.error('Error fetching versions:', error);
throw error;
html += '</div></div>';
}
container.innerHTML = html;
// 显示"加载更多"按钮
if (versions.length < allVersions.length) {
var btn = document.createElement('button');
btn.className = 'load-more';
btn.textContent = '加载更多(共 ' + allVersions.length + ' 个版本)';
btn.onclick = function() { renderVersionList(allVersions); };
container.appendChild(btn);
}
}
// 渲染版本列表
function renderVersionList(
versions,
latestVersion,
currentVersion,
cdnType
) {
const container = document.getElementById('versionList');
// 只显示最近 20 个版本
const displayVersions = versions.slice(0, 20);
const html = displayVersions
.map((version) => {
const isCurrent = version === currentVersion;
const isLatest = version === latestVersion;
const url = getVersionUrl(version, cdnType);
let tags = '';
if (isLatest) {
tags += '<span class="version-tag latest">最新</span>';
}
if (isCurrent) {
tags += '<span class="version-tag current">当前</span>';
}
return `
<a href="${url}" class="version-card ${isCurrent ? 'current' : ''}" ${
isCurrent ? '' : ''
}>
<div class="version-info">
<span class="version-number">v${version}</span>
${tags}
</div>
<div class="version-action">
${isCurrent ? '当前版本' : '使用此版本'}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18l6-6-6-6"/>
</svg>
</div>
</a>
`;
})
.join('');
container.innerHTML = `<div class="version-list">${html}</div>`;
}
// 更新 CDN 链接
function updateCdnLinks(latestVersion) {
const unpkgLink = document.getElementById('unpkgLink');
const jsdelivrLink = document.getElementById('jsdelivrLink');
unpkgLink.href = `https://unpkg.com/${PACKAGE_NAME}@${latestVersion}/index.html`;
unpkgLink.textContent = `unpkg: https://unpkg.com/${PACKAGE_NAME}@${latestVersion}/`;
jsdelivrLink.href = `https://cdn.jsdelivr.net/npm/${PACKAGE_NAME}@${latestVersion}/index.html`;
jsdelivrLink.textContent = `jsdelivr: https://cdn.jsdelivr.net/npm/${PACKAGE_NAME}@${latestVersion}/`;
}
// 初始化
async function init() {
const currentVersion = getCurrentVersion();
const cdnType = getCdnType();
// 显示当前版本
document.getElementById('currentVersion').textContent = currentVersion
? `v${currentVersion}`
: '未知';
try {
const { versions, latest } = await fetchVersions();
renderVersionList(versions, latest, currentVersion, cdnType);
updateCdnLinks(latest);
var response = await fetch('./changelog.json');
if (!response.ok) throw new Error('Failed to fetch changelog');
var data = await response.json();
allVersions = data.versions || [];
renderVersionList(allVersions.slice(0, PAGE_SIZE));
} catch (error) {
document.getElementById('versionList').innerHTML = `
<div class="error">
<p>加载版本列表失败</p>
<p style="margin-top: 10px; font-size: 13px;">请检查网络连接或访问
<a href="https://www.npmjs.com/package/${PACKAGE_NAME}?activeTab=versions" target="_blank">npm</a>
查看所有版本
</p>
</div>
`;
document.getElementById('versionList').innerHTML =
'<div class="error"><p>加载版本日志失败</p>' +
'<p style="margin-top:10px;font-size:13px;">请检查网络连接</p></div>';
}
}
@@ -292,21 +292,30 @@ export const UserManual = () => {
};
UserManual.displayName = 'UserManual';
export const ChangeLog = () => {
const { t } = useI18n();
return (
<MenuItem
icon={<BookOpenIcon />}
data-track="toolbar_click_menu_changelog"
onSelect={() => {
window.open('./versions.html', '_blank');
}}
aria-label={t('menu.changelog')}
>
{t('menu.changelog')}
</MenuItem>
);
};
ChangeLog.displayName = 'ChangeLog';
export const VersionInfo = () => {
const { t } = useI18n();
// 从 HTML meta 标签获取版本号
const version = document.querySelector('meta[name="app-version"]')?.getAttribute('content') || '0.0.0';
// 根据域名决定跳转地址
const getVersionsUrl = () => {
const hostname = window.location.hostname;
// unpkg.com 或 cdn.jsdelivr.net 域名使用内置版本选择页面
if (hostname.includes('unpkg.com') || hostname.includes('jsdelivr.net')) {
return './versions.html';
}
// 其他域名跳转到官方发布页面
return 'https://release.opentu.ai/';
};
// 跳转到 release 站点查看所有版本
const getVersionsUrl = () => 'https://release.opentu.ai/';
return (
<MenuItem
@@ -14,7 +14,7 @@ import {
import { Island } from '../../island';
import { Popover, PopoverContent, PopoverTrigger } from '../../popover/popover';
import { useState } from 'react';
import { CleanBoard, OpenFile, SaveAsImage, SaveToFile, Settings, BackupRestore, CloudSync, DebugPanel, QuickCommands, UserManual, VersionInfo, CleanInvalidLinks } from './app-menu-items';
import { CleanBoard, OpenFile, SaveAsImage, SaveToFile, Settings, BackupRestore, CloudSync, DebugPanel, QuickCommands, UserManual, ChangeLog, VersionInfo, CleanInvalidLinks } from './app-menu-items';
import { GithubIcon } from '../../icons';
import { LanguageSwitcherMenu } from './language-switcher-menu';
import Menu from '../../menu/menu';
@@ -107,6 +107,7 @@ export const AppToolbar: React.FC<AppToolbarProps> = ({
<MenuSeparator />
<QuickCommands />
<UserManual />
<ChangeLog />
<VersionInfo />
</Menu>
</PopoverContent>
+3
View File
@@ -85,6 +85,7 @@ export interface Translations {
'menu.more': string;
'menu.commandPalette': string;
'menu.userManual': string;
'menu.changelog': string;
'menu.cleanInvalidLinks': string;
'menu.cleanInvalidLinks.scanning': string;
'menu.cleanInvalidLinks.success': string;
@@ -271,6 +272,7 @@ const translations: Record<Language, Translations> = {
'menu.more': '更多',
'menu.commandPalette': '快捷命令',
'menu.userManual': '用户手册',
'menu.changelog': '版本日志',
'menu.cleanInvalidLinks': '清除失效媒体',
'menu.cleanInvalidLinks.scanning': '正在扫描失效媒体...',
'menu.cleanInvalidLinks.success': '已清除 {count} 个失效媒体',
@@ -454,6 +456,7 @@ const translations: Record<Language, Translations> = {
'menu.more': 'More',
'menu.commandPalette': 'Commands',
'menu.userManual': 'User Manual',
'menu.changelog': 'Changelog',
'menu.cleanInvalidLinks': 'Clean Invalid Media',
'menu.cleanInvalidLinks.scanning': 'Scanning invalid media...',
'menu.cleanInvalidLinks.success': 'Cleaned {count} invalid media',
+8 -1
View File
@@ -308,10 +308,17 @@ function main() {
console.log(`ℹ️ 没有找到提交记录,跳过 CHANGELOG 更新`);
}
// 同步 CHANGELOG.md → changelog.json
try {
require('./sync-changelog')();
} catch (error) {
console.warn(`⚠️ 同步 changelog.json 失败:`, error.message);
}
// 提交更改
try {
// 检查是否存在 package-lock.json 或 pnpm-lock.yaml
const filesToAdd = ['package.json', 'apps/web/public/version.json', 'CHANGELOG.md'];
const filesToAdd = ['package.json', 'apps/web/public/version.json', 'CHANGELOG.md', 'apps/web/public/changelog.json'];
if (fs.existsSync('package-lock.json')) {
filesToAdd.push('package-lock.json');
} else if (fs.existsSync('pnpm-lock.yaml')) {
+159
View File
@@ -0,0 +1,159 @@
/**
* sync-changelog.js
* 解析 CHANGELOG.md 生成 apps/web/public/changelog.json
*
* 用法:
* node scripts/sync-changelog.js # 独立运行
* require('./sync-changelog')() # 在其他脚本中调用
*/
const fs = require('fs');
const path = require('path');
function compareSemver(a, b) {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (pa[i] !== pb[i]) return pa[i] - pb[i];
}
return 0;
}
function detectType(version, prevVersion) {
if (!prevVersion) return 'patch';
const [aMaj, aMin] = version.split('.').map(Number);
const [bMaj, bMin] = prevVersion.split('.').map(Number);
if (aMaj !== bMaj) return 'major';
if (aMin !== bMin) return 'minor';
return 'patch';
}
// 去掉行末 commit hash 链接,如 ([abc1234](https://...))
function stripCommitLink(line) {
return line.replace(/\s*\(\[[a-f0-9]+\]\([^)]+\)\)\s*$/, '').trim();
}
// 判断条目是否为噪音(对用户无价值)
function isNoiseEntry(text) {
// 版本号 bump
if (/^(bump|update)\s+(app\s+)?version\s+to\s/i.test(text)) return true;
if (/^更新(应用)?版本至/i.test(text)) return true;
// docs 类(CLAUDE.md、编码规则等)
if (/^docs(\(.*?\))?:/i.test(text)) return true;
// 纯 refactor/style/ci/build/test/revert 前缀
if (/^(refactor|style|ci|build|test|revert)(\(.*?\))?:/i.test(text)) return true;
// 更新 build time
if (/update.*build\s*time/i.test(text)) return true;
// 过短无意义(如 "升级"、"ppt"、"简化方案")
if (text.length <= 4) return true;
// 纯英文的内部优化描述(enhance/improve/optimize/unify 开头,不含中文)
const hasChinese = /[\u4e00-\u9fa5]/.test(text);
if (!hasChinese && /^(enhance|improve|optimize|unify)\s/i.test(text)) return true;
return false;
}
function parseChangelog(content) {
const versions = [];
// 按 ## VERSION (DATE) 分割
const versionRegex = /^## (\d+\.\d+\.\d+)\s+\((\d{4}-\d{2}-\d{2})\)/;
const sectionRegex = /^### (.+)/;
const lines = content.split('\n');
let current = null;
let currentSection = null;
for (const line of lines) {
const vMatch = line.match(versionRegex);
if (vMatch) {
if (current) versions.push(current);
current = {
version: vMatch[1],
date: vMatch[2],
changes: { features: [], fixes: [], improvements: [] },
};
currentSection = null;
continue;
}
if (!current) continue;
const sMatch = line.match(sectionRegex);
if (sMatch) {
const title = sMatch[1].trim();
if (/Thank You/i.test(title)) {
currentSection = null; // 跳过 Thank You 段落
} else if (/Features/i.test(title)) {
currentSection = 'features';
} else if (/Fixes/i.test(title)) {
currentSection = 'fixes';
} else if (/Chores|Improvements/i.test(title)) {
currentSection = 'improvements';
} else {
currentSection = null;
}
continue;
}
// 解析列表项
const itemMatch = line.match(/^- (.+)/);
if (itemMatch && currentSection && current.changes[currentSection]) {
const text = stripCommitLink(itemMatch[1]);
if (text) {
current.changes[currentSection].push(text);
}
}
}
if (current) versions.push(current);
// 按版本号降序排列
versions.sort((a, b) => compareSemver(b.version, a.version));
// 计算 type 和 highlights
for (let i = 0; i < versions.length; i++) {
const prev = i < versions.length - 1 ? versions[i + 1].version : null;
versions[i].type = detectType(versions[i].version, prev);
// 过滤噪音条目
const c = versions[i].changes;
c.features = c.features.filter(t => !isNoiseEntry(t));
c.fixes = c.fixes.filter(t => !isNoiseEntry(t));
c.improvements = c.improvements.filter(t => !isNoiseEntry(t));
const { features, fixes } = c;
versions[i].highlights =
(features.length > 0 && features[0]) ||
(fixes.length > 0 && fixes[0]) ||
'构建修复与稳定性改进';
// 清理空的 changes
if (!c.features.length && !c.fixes.length && !c.improvements.length) {
delete versions[i].changes;
}
}
return { versions };
}
function syncChangelog() {
const changelogPath = path.join(__dirname, '../CHANGELOG.md');
const outputPath = path.join(__dirname, '../apps/web/public/changelog.json');
if (!fs.existsSync(changelogPath)) {
console.log('ℹ️ CHANGELOG.md 不存在,跳过同步');
return;
}
const content = fs.readFileSync(changelogPath, 'utf8');
const data = parseChangelog(content);
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2) + '\n');
console.log(`✅ changelog.json 已同步(${data.versions.length} 个版本)`);
}
// 支持独立运行和 require() 调用
if (require.main === module) {
syncChangelog();
} else {
module.exports = syncChangelog;
}
+21
View File
@@ -74,6 +74,26 @@ function updateHtmlWithVersion(version) {
console.log(`✅ HTML updated with version ${version}`);
}
// 检查 changelog.json 是否包含当前版本
function checkChangelog(version) {
const changelogPath = path.join(__dirname, '../apps/web/public/changelog.json');
if (!fs.existsSync(changelogPath)) {
console.log(`⚠️ changelog.json not found, skipping changelog check`);
return;
}
try {
const changelog = JSON.parse(fs.readFileSync(changelogPath, 'utf8'));
const hasEntry = (changelog.versions || []).some(v => v.version === version);
if (!hasEntry) {
console.log(`⚠️ Version ${version} has no entry in changelog.json — consider adding one`);
} else {
console.log(`✅ changelog.json has entry for ${version}`);
}
} catch (e) {
console.log(`⚠️ Failed to parse changelog.json: ${e.message}`);
}
}
// 主函数
function main() {
const version = getCurrentVersion();
@@ -83,6 +103,7 @@ function main() {
// updateServiceWorkerVersion(version);
createVersionFile(version);
updateHtmlWithVersion(version);
checkChangelog(version);
console.log(`🎉 Version update completed: ${version}`);
}
+3 -1
View File
@@ -363,7 +363,9 @@ if [ -f "${releasesDir}/$VERSION/version.json" ] && [ -d "${releasesDir}/$VERSIO
else
echo "解压验证失败"
exit 1
fi`;
fi
cp "${releasesDir}/$VERSION/versions.html" "${releasesDir}/versions.html" 2>/dev/null || true
cp "${releasesDir}/$VERSION/changelog.json" "${releasesDir}/changelog.json" 2>/dev/null || true`;
// 将脚本编码为 base64,避免引号转义问题
const encodedScript = Buffer.from(extractScript).toString('base64');