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
JavaScript
/**
* 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;