1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
require('dotenv').config(); const fs = require('fs'); const path = require('path'); const axios = require('axios'); const { glob } = require('glob');
const config = { api: process.env.AI_SUMMARY_API, key: process.env.AI_SUMMARY_KEY || '', model: process.env.AI_SUMMARY_MODEL || 'lite', concurrency: parseInt(process.env.AISUMMARY_CONCURRENCY) || 1, coverAll: process.env.AISUMMARY_COVER_ALL === 'true', maxToken: parseInt(process.env.AISUMMARY_MAX_TOKEN) || 5000, minContentLength: parseInt(process.env.AISUMMARY_MIN_CONTENT_LENGTH) || 50, prompt: process.env.AI_SUMMARY_PROMPT || '请为以下文章生成一个简洁的摘要,100-200字左右,突出重点内容:' };
function readFile(filePath) { try { return fs.readFileSync(filePath, 'utf-8'); } catch (err) { console.error('读取文件失败:', filePath, err.message); return null; } }
function parseFrontmatter(content) { const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); if (!match) return { data: {}, content: content }; const yaml = match[1]; const body = content.slice(match[0].length); const data = {}; yaml.split('\n').forEach(line => { const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const key = line.slice(0, colonIndex).trim(); let value = line.slice(colonIndex + 1).trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } data[key] = value; } }); return { data, content: body }; }
function stringifyFrontmatter(data) { const lines = ['---']; for (const [key, value] of Object.entries(data)) { if (value === undefined || value === null) continue; if (typeof value === 'string' && (value.includes(':') || value.includes('\n'))) { lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`); } else { lines.push(`${key}: ${value}`); } } lines.push('---'); return lines.join('\n'); }
// 提取纯文本内容(移除 Markdown 标记和代码) function extractText(content) { return content .replace(/!\[.*?\]\(.*?\)/g, '') // 移除图片 .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // 移除链接,保留文字 .replace(/```[\s\S]*?```/g, '') // 移除代码块 .replace(/`([^`]+)`/g, '$1') // 移除行内代码 .replace(/[#*\-_>]/g, '') // 移除 Markdown 标记 .replace(/\s+/g, ' ') // 合并空白 .trim(); }
// 延迟函数 function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
// 生成摘要(带重试) async function generateSummary(text, retries = 3) { for (let i = 0; i < retries; i++) { try { const requestBody = { model: config.model, content: config.prompt + '\n\n' + text.slice(0, config.maxToken) }; const response = await axios.post(config.api, requestBody, { headers: { 'Content-Type': 'application/json', ...(config.key && { 'Authorization': `Bearer ${config.key}` }) }, timeout: 60000 });
if (response.data && response.data.summary) { return response.data.summary.trim(); } if (response.data && response.data.content) { return response.data.content.trim(); } throw new Error('Invalid response format'); } catch (err) { const isLastRetry = i === retries - 1; // 限流错误处理 if (err.response?.data?.error?.includes('QpsOverFlow') || err.response?.data?.error?.includes('ConcurrencyOverFlow')) { if (!isLastRetry) { const waitTime = (i + 1) * 2000; console.log(` API 限流,等待 ${waitTime/1000} 秒后重试...`); await delay(waitTime); continue; } } throw err; } } }
// 处理单个文件 async function processFile(filePath) { console.log('处理文件:', filePath); const content = readFile(filePath); if (!content) return; const { data, content: body } = parseFrontmatter(content); // 检查是否已有摘要且不覆盖 if (data.ai_summary && !config.coverAll) { console.log(' 已有摘要,跳过'); return; } // 提取文本 const text = extractText(body); if (text.length < config.minContentLength) { console.log(' 内容太短,跳过'); return; } try { console.log(' 生成摘要中...'); let summary = await generateSummary(text); // 清理摘要格式 summary = summary .replace(/\n+/g, ' ') .replace(/\s+/g, ' ') .trim(); if (summary.length > 500) { summary = summary.slice(0, 500) + '...'; } // 更新 frontmatter data.ai_summary = summary; // 写回文件 const newContent = stringifyFrontmatter(data) + '\n' + body; fs.writeFileSync(filePath, newContent, 'utf-8'); console.log(' 摘要生成成功:', summary.slice(0, 50) + '...'); } catch (err) { console.error(' 生成摘要失败:', err.message); } }
// 主函数 async function main() { console.log('开始生成 AI 摘要...\n'); const files = await glob('source/_posts/**/*.md'); console.log(`找到 ${files.length} 篇文章\n`); for (const file of files) { await processFile(file); } console.log('\nAI 摘要生成完成!'); }
main().catch(err => { console.error('程序出错:', err); process.exit(1); });
|