Files
aitu/scripts/safe-version-bump.js
gongchengtu 51bb033edb 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
2026-02-26 15:01:02 +08:00

351 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
function getCurrentVersion() {
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
return packageJson.version;
}
function getNextVersion(currentVersion, type = 'patch') {
const parts = currentVersion.split('.').map(Number);
switch (type) {
case 'major':
parts[0]++;
parts[1] = 0;
parts[2] = 0;
break;
case 'minor':
parts[1]++;
parts[2] = 0;
break;
case 'patch':
default:
parts[2]++;
break;
}
return parts.join('.');
}
function tagExists(version) {
try {
execSync(`git tag -l | grep -q "^v${version}$"`, { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
function findNextAvailableVersion(baseVersion, type) {
let version = getNextVersion(baseVersion, type);
// 如果 tag 已存在,继续递增直到找到可用的版本号
while (tagExists(version)) {
console.log(`⚠️ 版本 v${version} 已存在,尝试下一个版本...`);
version = getNextVersion(version, 'patch'); // 总是递增 patch 版本
}
return version;
}
function updatePackageVersion(newVersion) {
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
packageJson.version = newVersion;
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
}
// 更新 Service Worker 中的版本号(已废弃,sw.js 现在是构建产物)
function updateServiceWorkerVersion(version) {
// sw.js 现在是构建产物,不再需要手动更新
// 版本信息会通过 version.json 和构建过程自动处理
console.log(`ℹ️ Service Worker 版本将通过构建过程自动更新`);
}
// 计算两个字符串的相似度 (Jaccard Index based on characters)
function calculateSimilarity(str1, str2) {
const set1 = new Set(str1.split(''));
const set2 = new Set(str2.split(''));
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
// 创建版本信息文件
function createVersionFile(version, commits) {
let changelog = [];
if (commits) {
// 过滤掉用户无感的提交类型 (docs, test, style, refactor, ci, build, revert)
const irrelevantRegex = /^(docs|test|style|refactor|ci|build|revert|chore)(\(.*?\))?:/i;
const relevantOthers = commits.others.filter(c => !c.message.match(irrelevantRegex));
const allCommits = [
...commits.features,
...commits.fixes,
// ...commits.chores, // 排除 chores
...relevantOthers
];
// 1. 过滤出有意义的提交消息(汉字 > 5 个 或 英文单词 > 5 个)
const filteredCommits = allCommits
.filter(c => {
const chineseChars = c.message.match(/[\u4e00-\u9fa5]/g) || [];
// 提取英文单词(排除常见的 commit 前缀如 feat/fix/chore 等)
const cleanMessage = c.message.replace(/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]*\))?:\s*/i, '');
const englishWords = cleanMessage.match(/[a-zA-Z]{2,}/g) || [];
return chineseChars.length > 5 || englishWords.length > 5;
})
.map(c => c.message);
// 2. 按长度降序排序(文字多的优先级高)
filteredCommits.sort((a, b) => b.length - a.length);
// 3. 去重和过滤相似消息
const uniqueChangelog = [];
for (const msg of filteredCommits) {
let isSimilar = false;
for (const existingMsg of uniqueChangelog) {
// 如果包含关系(忽略大小写和空格)
if (existingMsg.toLowerCase().replace(/\s/g, '').includes(msg.toLowerCase().replace(/\s/g, ''))) {
isSimilar = true;
break;
}
// 如果相似度超过 0.6 (60% 相似)
if (calculateSimilarity(msg, existingMsg) > 0.6) {
isSimilar = true;
break;
}
}
if (!isSimilar) {
uniqueChangelog.push(msg);
}
}
changelog = uniqueChangelog;
}
const versionInfo = {
version: version,
buildTime: new Date().toISOString(),
gitCommit: process.env.GITHUB_SHA || 'unknown',
changelog: changelog
};
const versionPath = path.join(__dirname, '../apps/web/public/version.json');
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2));
console.log(`✅ 版本信息文件已创建: ${version} (包含 ${changelog.length} 条更新日志)`);
}
// 获取上一个版本号
function getPreviousVersion(currentVersion) {
try {
// 尝试获取上一个版本的 tag
const tags = execSync('git tag --sort=-version:refname', { encoding: 'utf8' })
.trim()
.split('\n')
.filter(tag => tag.startsWith('v'));
// 找到当前版本之前的版本
const currentTag = `v${currentVersion}`;
const currentIndex = tags.indexOf(currentTag);
if (currentIndex > 0 && currentIndex < tags.length) {
return tags[currentIndex + 1].substring(1); // 移除 'v' 前缀
}
// 如果找不到,返回最新的 tag
if (tags.length > 0) {
return tags[0].substring(1);
}
} catch (error) {
console.warn('⚠️ 无法获取上一个版本:', error.message);
}
return null;
}
// 获取提交记录并分类
function getCommitsSinceLastVersion(lastVersion) {
try {
let gitCommand;
if (lastVersion) {
gitCommand = `git log v${lastVersion}..HEAD --pretty=format:"%s|||%h|||%an|||%ae" --no-merges`;
} else {
// 如果没有上一个版本,获取最近20条提交
gitCommand = `git log -20 --pretty=format:"%s|||%h|||%an|||%ae" --no-merges`;
}
const commits = execSync(gitCommand, { encoding: 'utf8' })
.trim()
.split('\n')
.filter(line => line.length > 0);
// 分类提交
const categorized = {
features: [],
fixes: [],
chores: [],
others: [],
authors: new Set()
};
commits.forEach(commit => {
const [message, hash, authorName, authorEmail] = commit.split('|||');
// 收集作者信息
categorized.authors.add(`${authorName} <${authorEmail}>`);
// 根据 conventional commits 规范分类
if (message.match(/^feat(\(.*?\))?:/i)) {
categorized.features.push({ message: message.replace(/^feat(\(.*?\))?:\s*/i, ''), hash });
} else if (message.match(/^fix(\(.*?\))?:/i)) {
categorized.fixes.push({ message: message.replace(/^fix(\(.*?\))?:\s*/i, ''), hash });
} else if (message.match(/^chore(\(.*?\))?:/i)) {
categorized.chores.push({ message: message.replace(/^chore(\(.*?\))?:\s*/i, ''), hash });
} else {
categorized.others.push({ message, hash });
}
});
return categorized;
} catch (error) {
console.warn('⚠️ 无法获取提交记录:', error.message);
return null;
}
}
// 更新 CHANGELOG.md
function updateChangelog(version, commits) {
const changelogPath = path.join(__dirname, '../CHANGELOG.md');
const date = new Date().toISOString().split('T')[0];
// 构建新的 changelog 条目
let newEntry = `## ${version} (${date})\n\n`;
// 添加功能
if (commits.features.length > 0) {
newEntry += `### 🚀 Features\n\n`;
commits.features.forEach(({ message, hash }) => {
newEntry += `- ${message} ([${hash}](https://github.com/ljquan/aitu/commit/${hash}))\n`;
});
newEntry += '\n';
}
// 添加修复
if (commits.fixes.length > 0) {
newEntry += `### 🩹 Fixes\n\n`;
commits.fixes.forEach(({ message, hash }) => {
newEntry += `- ${message} ([${hash}](https://github.com/ljquan/aitu/commit/${hash}))\n`;
});
newEntry += '\n';
}
// 添加其他更改
if (commits.chores.length > 0 || commits.others.length > 0) {
newEntry += `### 🔧 Chores\n\n`;
[...commits.chores, ...commits.others].forEach(({ message, hash }) => {
newEntry += `- ${message} ([${hash}](https://github.com/ljquan/aitu/commit/${hash}))\n`;
});
newEntry += '\n';
}
// 添加贡献者
if (commits.authors.size > 0) {
newEntry += `### ❤️ Thank You\n\n`;
Array.from(commits.authors).forEach(author => {
newEntry += `- ${author}\n`;
});
newEntry += '\n';
}
// 读取现有 CHANGELOG
let changelogContent = '';
if (fs.existsSync(changelogPath)) {
changelogContent = fs.readFileSync(changelogPath, 'utf8');
}
// 插入新条目到文件开头
const updatedChangelog = newEntry + changelogContent;
fs.writeFileSync(changelogPath, updatedChangelog);
console.log(`✅ CHANGELOG.md 已更新`);
}
function main() {
const versionType = process.argv[2] || 'patch';
try {
const currentVersion = getCurrentVersion();
console.log(`📦 当前版本: ${currentVersion}`);
const nextVersion = findNextAvailableVersion(currentVersion, versionType);
console.log(`🚀 升级到版本: ${nextVersion}`);
// 更新 package.json
updatePackageVersion(nextVersion);
console.log(`✅ package.json 已更新到 ${nextVersion}`);
// 更新 Service Worker 版本
updateServiceWorkerVersion(nextVersion);
// 获取并更新 CHANGELOG
const previousVersion = getPreviousVersion(currentVersion);
console.log(`📝 从版本 ${previousVersion || '开始'} 收集提交记录...`);
const commits = getCommitsSinceLastVersion(previousVersion);
// 创建版本信息文件
createVersionFile(nextVersion, commits);
if (commits && (commits.features.length > 0 || commits.fixes.length > 0 || commits.chores.length > 0 || commits.others.length > 0)) {
updateChangelog(nextVersion, commits);
} else {
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', 'apps/web/public/changelog.json'];
if (fs.existsSync('package-lock.json')) {
filesToAdd.push('package-lock.json');
} else if (fs.existsSync('pnpm-lock.yaml')) {
filesToAdd.push('pnpm-lock.yaml');
}
execSync(`git add ${filesToAdd.join(' ')}`, { stdio: 'inherit' });
execSync(`git commit -m "chore: bump version to ${nextVersion}"`, { stdio: 'inherit' });
console.log(`✅ 版本更改已提交`);
} catch (error) {
console.log(`ℹ️ 跳过 git 提交(可能没有更改或不在 git 仓库中)`);
}
// 创建 tag
try {
execSync(`git tag -a v${nextVersion} -m "Release ${nextVersion}"`, { stdio: 'inherit' });
console.log(`✅ 创建 git tag: v${nextVersion}`);
} catch (error) {
console.log(`⚠️ 创建 git tag 失败: ${error.message}`);
}
console.log(`\n🎉 版本升级完成: ${currentVersion} → ${nextVersion}`);
} catch (error) {
console.error(`❌ 版本升级失败:`, error.message);
process.exit(1);
}
}
main();