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:
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
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user