UNPKG

hexo-ai-summary-pro

Version:

Professional AI summary generator for Hexo - supports multiple AI providers with automatic generation

366 lines (326 loc) 11.2 kB
/** * AI摘要生成器核心类 * 支持多种AI服务提供商 */ 'use strict'; const https = require('https'); const http = require('http'); const { URL } = require('url'); // AI服务提供商配置 const AI_PROVIDERS = { openai: { name: 'OpenAI', defaultModel: 'gpt-3.5-turbo', endpoint: 'https://api.openai.com/v1/chat/completions' }, claude: { name: 'Claude', defaultModel: 'claude-3-haiku-20240307', endpoint: 'https://api.anthropic.com/v1/messages' }, gemini: { name: 'Google Gemini', defaultModel: 'gemini-pro', endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent' }, qianwen: { name: '通义千问', defaultModel: 'qwen-turbo', endpoint: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation' }, baidu: { name: '文心一言', defaultModel: 'ERNIE-Bot-turbo', endpoint: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant' }, custom: { name: '自定义API', defaultModel: 'custom-model', endpoint: '' } }; class AISummaryGenerator { constructor(hexo) { this.hexo = hexo; this.config = this.loadConfig(); this.log = hexo.log; } // 加载配置 loadConfig() { const defaultConfig = { enable: true, auto_generate: true, provider: 'openai', api_key: '', api_url: '', model: '', max_tokens: 150, temperature: 0.3, timeout: 30000, retry_count: 3, retry_delay: 1000, batch_delay: 500, prompt_template: '请为以下文章生成一个简洁的中文摘要,控制在150字以内,突出文章的核心技术点和主要内容:\n\n标题:{title}\n\n内容:{content}', exclude_layouts: ['page', 'about', 'links'], exclude_tags: [], exclude_categories: [], min_content_length: 100, fallback_summary: '本文介绍了相关技术内容,包含详细的实现方法和实用案例。' }; const userConfig = this.hexo.config.ai_summary_api || {}; const config = Object.assign({}, defaultConfig, userConfig); // 设置默认值 if (!config.model && AI_PROVIDERS[config.provider]) { config.model = AI_PROVIDERS[config.provider].defaultModel; } if (!config.api_url && AI_PROVIDERS[config.provider]) { config.api_url = AI_PROVIDERS[config.provider].endpoint; } return config; } // 文本预处理 preprocessContent(content) { return content .replace(/```[\s\S]*?```/g, '') .replace(/`[^`]*`/g, '') .replace(/<[^>]*>/g, '') .replace(/!\[.*?\]\(.*?\)/g, '') .replace(/\[.*?\]\(.*?\)/g, '') .replace(/#{1,6}\s/g, '') .replace(/\*\*.*?\*\*/g, '') .replace(/\*.*?\*/g, '') .replace(/\n+/g, ' ') .replace(/\s+/g, ' ') .trim() .substring(0, 3000); } // 构建API请求 buildAPIRequest(content, title) { const prompt = this.config.prompt_template .replace('{content}', content) .replace('{title}', title || ''); switch (this.config.provider) { case 'openai': return { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.api_key}` }, body: JSON.stringify({ model: this.config.model, messages: [{ role: 'user', content: prompt }], max_tokens: this.config.max_tokens, temperature: this.config.temperature }) }; case 'claude': return { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this.config.api_key, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: this.config.model, max_tokens: this.config.max_tokens, messages: [{ role: 'user', content: prompt }] }) }; case 'gemini': return { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { maxOutputTokens: this.config.max_tokens, temperature: this.config.temperature } }), url: `${this.config.api_url}?key=${this.config.api_key}` }; case 'qianwen': return { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.api_key}` }, body: JSON.stringify({ model: this.config.model, input: { messages: [{ role: 'user', content: prompt }] }, parameters: { max_tokens: this.config.max_tokens, temperature: this.config.temperature } }) }; case 'baidu': return { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: prompt }], max_output_tokens: this.config.max_tokens, temperature: this.config.temperature }), url: `${this.config.api_url}?access_token=${this.config.api_key}` }; case 'custom': return { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.api_key}` }, body: JSON.stringify({ model: this.config.model, messages: [{ role: 'user', content: prompt }], max_tokens: this.config.max_tokens, temperature: this.config.temperature, stream: false }) }; default: throw new Error(`不支持的AI服务: ${this.config.provider}`); } } // 发送HTTP请求 async makeRequest(requestConfig) { return new Promise((resolve, reject) => { const url = new URL(requestConfig.url || this.config.api_url); const client = url.protocol === 'https:' ? https : http; const options = { hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method: requestConfig.method, headers: requestConfig.headers, timeout: this.config.timeout }; const req = client.request(options, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { resolve({ statusCode: res.statusCode, data: JSON.parse(data) }); } catch (error) { reject(new Error(`解析响应失败: ${error.message}`)); } }); }); req.on('error', (error) => reject(new Error(`请求失败: ${error.message}`))); req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); }); if (requestConfig.body) { req.write(requestConfig.body); } req.end(); }); } // 解析API响应 parseResponse(response, provider) { try { switch (provider) { case 'openai': return response.choices?.[0]?.message?.content?.trim(); case 'claude': return response.content?.[0]?.text?.trim(); case 'gemini': return response.candidates?.[0]?.content?.parts?.[0]?.text?.trim(); case 'qianwen': return response.output?.text?.trim(); case 'baidu': return response.result?.trim(); case 'custom': return response.choices?.[0]?.message?.content?.trim() || response.content?.trim() || response.text?.trim() || response.result?.trim() || response.output?.trim(); default: throw new Error(`不支持的提供商: ${provider}`); } } catch (error) { throw new Error(`解析响应失败: ${error.message}`); } } // 生成摘要 async generateSummary(content, title) { if (!this.config.api_key) { this.log.warn('AI Summary: 未配置API密钥,使用默认摘要'); return this.config.fallback_summary; } const cleanContent = this.preprocessContent(content); if (!cleanContent) { return this.config.fallback_summary; } let lastError; for (let attempt = 1; attempt <= this.config.retry_count; attempt++) { try { this.log.info(`AI Summary: 正在生成摘要 (尝试 ${attempt}/${this.config.retry_count}) - ${title}`); const requestConfig = this.buildAPIRequest(cleanContent, title); const response = await this.makeRequest(requestConfig); if (response.statusCode !== 200) { throw new Error(`API返回错误状态码: ${response.statusCode}`); } const summary = this.parseResponse(response.data, this.config.provider); if (summary && summary.length > 10) { this.log.info(`AI Summary: 摘要生成成功 - ${title}`); return summary; } else { throw new Error('生成的摘要为空或过短'); } } catch (error) { lastError = error; this.log.warn(`AI Summary: 第${attempt}次尝试失败: ${error.message}`); if (attempt < this.config.retry_count) { await new Promise(resolve => setTimeout(resolve, this.config.retry_delay)); } } } this.log.error(`AI Summary: 所有尝试都失败了,使用默认摘要: ${lastError.message}`); return this.config.fallback_summary; } // 检查是否应该生成摘要 shouldGenerateSummary(data) { // 检查是否启用 if (!this.config.enable) return false; // 检查布局类型排除 if (this.config.exclude_layouts.includes(data.layout)) return false; // 如果已经有AI摘要,跳过 if (data.ai_text || data.ai_summary) { this.log.debug(`AI Summary: 文章已有摘要,跳过 - ${data.title}`); return false; } // 检查标签排除 if (data.tags && this.config.exclude_tags.length > 0) { const postTags = data.tags.data ? data.tags.data.map(tag => tag.name) : []; if (postTags.some(tag => this.config.exclude_tags.includes(tag))) { this.log.debug(`AI Summary: 文章标签被排除,跳过 - ${data.title}`); return false; } } // 检查分类排除 if (data.categories && this.config.exclude_categories.length > 0) { const postCategories = data.categories.data ? data.categories.data.map(cat => cat.name) : []; if (postCategories.some(cat => this.config.exclude_categories.includes(cat))) { this.log.debug(`AI Summary: 文章分类被排除,跳过 - ${data.title}`); return false; } } // 检查文章内容长度 if (!data.content || data.content.trim().length < this.config.min_content_length) { this.log.debug(`AI Summary: 文章内容过短,跳过 - ${data.title}`); return false; } return true; } } module.exports = AISummaryGenerator;