/** * Wiki文本清理工具 * 基于Angel Eye插件的实现,清理MediaWiki标记但保留核心内容结构 */ export class WikiTextCleaner { /** * 清理Wiki文本,移除视觉噪音但保留核心数据结构 * @param wikitext 原始Wiki文本 * @returns 清理和标准化后的文本 */ static clean(wikitext: string): string { if (!wikitext) { return ''; } let cleaned = wikitext; // 1. 移除纯视觉HTML标签(保留内容) cleaned = cleaned.replace(/<\/? (poem|del|big|small|u)[^>]*>/gi, ''); //
标签替换为换行符 cleaned = cleaned.replace(/ 标签移除 cleaned = cleaned.replace(/<\/?div[^>]*>/gi, ''); // 2. 移除页面级功能性模板 const pageLevelTemplates = [ /\{\{(原神TOP|背景图片|注释|references\/|玩梗适度)(\|[^}]*)?\}\}/gi ]; pageLevelTemplates.forEach(pattern => { cleaned = cleaned.replace(pattern, ''); }); // 3. 移除脚注 cleaned = cleaned.replace(/]*>.*?<\/ref>/gs, ''); // 4. 移除/展开内联样式模板(保留内容) // {{color|...|text}} -> text cleaned = cleaned.replace(/\{\{(?:color|genshincolor)\|[^|}]+?\|([^}]+?)\}\}/gi, '$1'); // {{ruby|text|pronunciation}} -> text(pronunciation) cleaned = cleaned.replace(/\{\{ruby\|([^|]+)\|([^}]+)\}\}/gi, '$1($2)'); // 5. 标准化数据分隔符 cleaned = cleaned.replace(/\{\{!!\}\}/g, ', '); // 6. 简化文件和外部链接 // [[File:...]] -> [图片: filename] cleaned = cleaned.replace(/\[\[File:([^|\]]+).*?\]\]/gi, '[图片: $1]'); // {{BilibiliVideo|id=...}} -> [Bilibili视频: URL] cleaned = cleaned.replace(/\{\{BilibiliVideo\|id=(.*?)\}\}/gi, '[Bilibili视频: https://www.bilibili.com/video/$1]'); // 7. 基础语法转换 // '''粗体''' -> **粗体** cleaned = cleaned.replace(/'''(.*?)'''/g, '**$1**'); // ''斜体'' -> *斜体* cleaned = cleaned.replace(/''(.*?)''/g, '*$1*'); // [[内部链接|链接文本]] -> [链接文本](内部链接) cleaned = cleaned.replace(/\[\[([^|\]]+?)\|([^\]]+?)\]\]/g, '[$2]($1)'); // [[内部链接]] -> [内部链接](内部链接) cleaned = cleaned.replace(/\[\[([^\]]+?)\]\]/g, '[$1]($1)'); // 标题转换 cleaned = cleaned.replace(/^======\s*(.*?)\s*======\s*$/gm, '###### $1'); cleaned = cleaned.replace(/^=====\s*(.*?)\s*=====\s*$/gm, '##### $1'); cleaned = cleaned.replace(/^====\s*(.*?)\s*====\s*$/gm, '#### $1'); cleaned = cleaned.replace(/^===\s*(.*?)\s*===\s*$/gm, '### $1'); cleaned = cleaned.replace(/^==\s*(.*?)\s*==\s*$/gm, '## $1'); cleaned = cleaned.replace(/^=\s*(.*?)\s*=\s*$/gm, '# $1'); // 小节标题 cleaned = cleaned.replace(/^;\s*(.*?)\s*$/gm, '**$1**'); // 外部链接 cleaned = cleaned.replace(/\[(https?:\/\/[^\s\]]+)\s+([^\]]+?)]/g, '[$2]($1)'); cleaned = cleaned.replace(/\[(https?:\/\/[^\s\]]+)]/g, '$1'); // 删除线 -> ~~删除线~~ cleaned = cleaned.replace(/(.*?)<\/del>/g, '~~$1~~'); // 8. 最终清理 // 合并多个换行符 cleaned = cleaned.replace(/\n{3,}/g, '\n\n'); // 移除首尾空白 cleaned = cleaned.trim(); return cleaned; } /** * 提取摘要文本(前N个字符) * @param wikitext 原始Wiki文本 * @param maxLength 最大长度,默认500字符 * @returns 摘要文本 */ static extractSummary(wikitext: string, maxLength: number = 500): string { const cleaned = this.clean(wikitext); if (cleaned.length <= maxLength) { return cleaned; } // 尝试在句子边界截断 const truncated = cleaned.substring(0, maxLength); const lastSentenceEnd = Math.max( truncated.lastIndexOf('。'), truncated.lastIndexOf('!'), truncated.lastIndexOf('?'), truncated.lastIndexOf('.'), truncated.lastIndexOf('!'), truncated.lastIndexOf('?') ); if (lastSentenceEnd > maxLength * 0.7) { return truncated.substring(0, lastSentenceEnd + 1); } // 如果没有合适的句子边界,在空格处截断 const lastSpace = truncated.lastIndexOf(' '); if (lastSpace > maxLength * 0.8) { return truncated.substring(0, lastSpace) + '...'; } return truncated + '...'; } /** * 提取关键信息(标题、第一段等) * @param wikitext 原始Wiki文本 * @returns 关键信息对象 */ static extractKeyInfo(wikitext: string): { title?: string; firstParagraph?: string; sections?: string[]; infobox?: string; } { const cleaned = this.clean(wikitext); const lines = cleaned.split('\n').filter(line => line.trim()); const result: any = {}; // 第一行通常作为标题 if (lines.length > 0) { result.title = lines[0].trim(); } // 第一段作为摘要 const firstEmptyLineIndex = lines.findIndex(line => !line.trim()); const firstParagraph = lines.slice(1, firstEmptyLineIndex > 0 ? firstEmptyLineIndex : 3).join(' ').trim(); if (firstParagraph) { result.firstParagraph = firstParagraph; } // 提取章节标题(以数字开头的行可能是章节) const sections = lines.filter(line => /^\d+\./.test(line.trim())); if (sections.length > 0) { result.sections = sections.slice(0, 5); // 最多取5个章节 } return result; } /** * 检查文本是否包含特定关键词 * @param wikitext Wiki文本 * @param keywords 关键词数组 * @returns 匹配的关键词数组 */ static findKeywords(wikitext: string, keywords: string[]): string[] { const cleaned = this.clean(wikitext).toLowerCase(); return keywords.filter(keyword => cleaned.includes(keyword.toLowerCase()) ); } /** * 格式化输出文本(适合CLI显示) * @param title 标题 * @param content 内容 * @param maxLength 最大长度 * @returns 格式化文本 */ static formatForDisplay(title: string, content: string, maxLength: number = 1000): string { const cleaned = this.clean(content); const summary = this.extractSummary(cleaned, maxLength); let result = `📖 ${title}\n`; result += '=' .repeat(title.length + 3) + '\n\n'; result += summary; if (cleaned.length > maxLength) { result += `\n\n... (内容已截断,完整长度: ${cleaned.length} 字符)`; } return result; } }