/** * GitHub 提交日志获取服务 * 用于从 GitHub API 获取项目的提交历史并转换为更新日志 */ export interface GitHubCommit { sha: string; commit: { author: { name: string; email: string; date: string; }; message: string; }; html_url: string; author: { login: string; avatar_url: string; } | null; } export interface ChangelogEntry { id: string; date: string; version?: string; author: { name: string; avatar?: string; username?: string; }; message: string; commitUrl: string; type: 'feature' | 'fix' | 'docs' | 'style' | 'refactor' | 'perf' | 'test' | 'chore' | 'update' | 'other'; scope?: string; } const GITHUB_API_BASE = 'https://api.github.com'; const REPO_OWNER = 'xiamuceer-j'; const REPO_NAME = 'MuMuAINovel'; /** * 提交类型映射表 * 统一不同别名到标准类型 */ const TYPE_MAPPING: Record = { // 功能类 'feat': 'feature', 'feature': 'feature', 'update': 'update', // 修复类 'fix': 'fix', // 文档类 'docs': 'docs', 'doc': 'docs', // 样式类 'style': 'style', // 重构类 'refactor': 'refactor', // 性能类 'perf': 'perf', // 测试类 'test': 'test', // 杂项类 'chore': 'chore', }; /** * 从提交信息中解析类型和作用域 * * 匹配优先级(从高到低): * 1. 标准 Conventional Commits 格式: type(scope): message 或 type: message * 2. 方括号格式: [type] message * 3. 简单前缀格式: type: message(支持中文冒号) * 4. 关键词模糊匹配(中英文) */ function parseCommitType(message: string): { type: ChangelogEntry['type']; scope?: string; cleanMessage: string } { const lowerMessage = message.toLowerCase().trim(); // 优先级1:标准 Conventional Commits 格式 - type(scope): message 或 type: message // 匹配所有支持的类型 const conventionalPattern = new RegExp( `^(${Object.keys(TYPE_MAPPING).join('|')})(?:\\(([^)]+)\\))?\\s*[:\\::]\\s*(.+)`, 'i' ); const conventionalMatch = message.match(conventionalPattern); if (conventionalMatch) { const typeStr = conventionalMatch[1].toLowerCase(); const mappedType = TYPE_MAPPING[typeStr] || 'other'; return { type: mappedType, scope: conventionalMatch[2], cleanMessage: conventionalMatch[3].trim(), }; } // 优先级2:方括号格式 - [type] message const bracketPattern = new RegExp( `^\\[(${Object.keys(TYPE_MAPPING).join('|')})\\]\\s*(.+)`, 'i' ); const bracketMatch = message.match(bracketPattern); if (bracketMatch) { const typeStr = bracketMatch[1].toLowerCase(); const mappedType = TYPE_MAPPING[typeStr] || 'other'; return { type: mappedType, cleanMessage: bracketMatch[2].trim(), }; } // 优先级3:简单前缀格式 - type: message(支持英文和中文冒号) for (const [key, value] of Object.entries(TYPE_MAPPING)) { const prefixPattern = new RegExp(`^${key}\\s*[:\\::]\\s*`, 'i'); if (prefixPattern.test(lowerMessage)) { const cleanMsg = message.replace(prefixPattern, '').trim(); return { type: value, cleanMessage: cleanMsg }; } } // 优先级4:关键词模糊匹配(仅当前面都不匹配时) const keywordMap: Array<{ keywords: string[]; type: ChangelogEntry['type'] }> = [ { keywords: ['修复', 'fix'], type: 'fix' }, { keywords: ['优化', 'perf'], type: 'perf' }, { keywords: ['文档', 'document'], type: 'docs' }, { keywords: ['新增', '添加', '增加', 'add'], type: 'feature' }, { keywords: ['更新', 'update'], type: 'update' }, { keywords: ['样式', 'style'], type: 'style' }, { keywords: ['重构', 'refactor'], type: 'refactor' }, { keywords: ['测试', 'test'], type: 'test' }, ]; for (const { keywords, type } of keywordMap) { if (keywords.some(keyword => lowerMessage.includes(keyword))) { return { type, cleanMessage: message }; } } // 默认类型 return { type: 'other', cleanMessage: message }; } /** * 获取GitHub提交历史 */ export async function fetchGitHubCommits(page: number = 1, perPage: number = 30): Promise { try { const url = `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/commits?author=${REPO_OWNER}&page=${page}&per_page=${perPage}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/vnd.github.v3+json', }, cache: 'no-cache', }); if (!response.ok) { throw new Error(`GitHub API 请求失败: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { console.error('获取 GitHub 提交历史失败:', error); throw error; } } /** * 将GitHub提交转换为更新日志条目 */ export function convertCommitsToChangelog(commits: GitHubCommit[]): ChangelogEntry[] { return commits.map(commit => { const { type, scope, cleanMessage } = parseCommitType(commit.commit.message); return { id: commit.sha, date: commit.commit.author.date, author: { name: commit.commit.author.name, avatar: commit.author?.avatar_url, username: commit.author?.login, }, message: cleanMessage, commitUrl: commit.html_url, type, scope, }; }); } /** * 获取格式化的更新日志 */ export async function fetchChangelog(page: number = 1, perPage: number = 30): Promise { const commits = await fetchGitHubCommits(page, perPage); return convertCommitsToChangelog(commits); } /** * 按日期分组更新日志 */ export function groupChangelogByDate(entries: ChangelogEntry[]): Map { const grouped = new Map(); entries.forEach(entry => { const date = new Date(entry.date).toISOString().split('T')[0]; const existing = grouped.get(date) || []; existing.push(entry); grouped.set(date, existing); }); return grouped; } /** * 检查是否应该获取更新日志(避免频繁请求) */ export function shouldFetchChangelog(): boolean { const lastFetch = localStorage.getItem('changelog_last_fetch'); if (!lastFetch) { return true; } const lastFetchTime = new Date(lastFetch).getTime(); const now = Date.now(); const oneHourMs = 60 * 60 * 1000; // 1小时 return now - lastFetchTime >= oneHourMs; } /** * 记录更新日志获取时间 */ export function markChangelogFetched(): void { localStorage.setItem('changelog_last_fetch', new Date().toISOString()); } /** * 获取缓存的更新日志 */ export function getCachedChangelog(): ChangelogEntry[] | null { const cached = localStorage.getItem('changelog_cache'); if (cached) { try { return JSON.parse(cached); } catch { return null; } } return null; } /** * 缓存更新日志 */ export function cacheChangelog(entries: ChangelogEntry[]): void { localStorage.setItem('changelog_cache', JSON.stringify(entries)); } /** * 清除更新日志缓存 * 用于强制刷新数据 */ export function clearChangelogCache(): void { localStorage.removeItem('changelog_cache'); localStorage.removeItem('changelog_last_fetch'); }