mirror of
https://github.com/Jerryplusy/crystelf-plugin.git
synced 2025-07-04 14:19:19 +00:00
rss解析?
This commit is contained in:
parent
7942c59a32
commit
4abe36912d
104
apps/rssPush.js
104
apps/rssPush.js
@ -0,0 +1,104 @@
|
|||||||
|
import configControl from '../lib/config/configControl.js';
|
||||||
|
import rssTools from '../models/rss/rss.js';
|
||||||
|
import path from 'path';
|
||||||
|
import screenshot from '../lib/rss/screenshot.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const rssCache = new Map();
|
||||||
|
|
||||||
|
export default class RssPlugin extends plugin {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
name: 'crystelf RSS订阅',
|
||||||
|
dsc: '定时推送rss解析流',
|
||||||
|
rule: [
|
||||||
|
{
|
||||||
|
reg: '^#rss添加(.+)$',
|
||||||
|
fnc: 'addFeed',
|
||||||
|
permission: 'master',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reg: '^#rss移除(\\d+)$',
|
||||||
|
fnc: 'removeFeed',
|
||||||
|
permission: 'master',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
task: [
|
||||||
|
{
|
||||||
|
name: 'RSS定时推送',
|
||||||
|
corn: '*/10 * * * *',
|
||||||
|
fnc: () => this.pushFeeds(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加rss
|
||||||
|
* @param e
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async addFeed(e) {
|
||||||
|
const url = e.msg.replace(/^#rss添加/, '').trim();
|
||||||
|
const feeds = configControl.get('feeds') || [];
|
||||||
|
const groupId = e.group_id;
|
||||||
|
|
||||||
|
const exists = feeds.find((f) => f.url === url);
|
||||||
|
if (exists) {
|
||||||
|
if (!exists.targetGroups.includes(groupId)) {
|
||||||
|
exists.targetGroups.push(groupId);
|
||||||
|
await configControl.set('feeds', feeds);
|
||||||
|
return e.reply(`群已添加到该rss订阅中..`, true);
|
||||||
|
}
|
||||||
|
return e.reply(`该rss已存在并包含在该群聊..`, true);
|
||||||
|
}
|
||||||
|
feeds.push({ url, targetGroup: [groupId], screenshot: true });
|
||||||
|
await configControl.set('feeds', feeds);
|
||||||
|
return e.reply(`rss解析流设置成功..`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除rss
|
||||||
|
* @param e
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async removeFeed(e) {
|
||||||
|
const index = parseInt(e.msg.replace(/^#rss移除/, '').trim(), 10);
|
||||||
|
const feeds = configControl.get('feeds') || [];
|
||||||
|
const groupId = e.group_id;
|
||||||
|
|
||||||
|
if (index < 0 || index >= feeds.length) return e.reply('索引无效');
|
||||||
|
|
||||||
|
feeds[index].targetGroups = feeds[index].targetGroups.filter((id) => id !== groupId);
|
||||||
|
await configControl.set('feeds', feeds);
|
||||||
|
return e.reply('群已移除该订阅');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查rss更新
|
||||||
|
* @param e
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async pushFeeds(e) {
|
||||||
|
const feeds = configControl.get('feeds') || [];
|
||||||
|
for (const feed of feeds) {
|
||||||
|
const latest = await rssTools.fetchFeed(feed.url);
|
||||||
|
if (!latest || latest.length) continue;
|
||||||
|
const cacheKey = feed.url;
|
||||||
|
const lastId = rssCache.get(cacheKey);
|
||||||
|
const newItems = lastId ? latest.filter((i) => i.link !== lastId) : latest;
|
||||||
|
if (newItems.length) rssCache.set(cacheKey, newItems[0].link);
|
||||||
|
for (const groupId of feed.targetGroups) {
|
||||||
|
const post = newItems[0];
|
||||||
|
const tempPath = path.join(process.cwd(), 'data', `rss-${Date.now()}.png`);
|
||||||
|
if (feed.screenshot) {
|
||||||
|
await screenshot.generateScreenshot(post, tempPath);
|
||||||
|
Bot.pickGroup(groupId)?.sendMsg([segment.image(tempPath)]);
|
||||||
|
fs.unlinkSync(tempPath);
|
||||||
|
} else {
|
||||||
|
Bot.pickGroup(groupId)?.sendMsg(`[RSS推送]\n${post.title}\n${post.link}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,43 +1,37 @@
|
|||||||
<!-- TODO 美观html -->
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: '微软雅黑', sans-serif;
|
font-family: "Helvetica Neue", sans-serif;
|
||||||
background: #f9f9f9;
|
margin: 30px;
|
||||||
padding: 40px;
|
background: #f5f7fa;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: auto;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 22px;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
p {
|
.container {
|
||||||
font-size: 16px;
|
background: white;
|
||||||
color: #666;
|
padding: 20px;
|
||||||
}
|
border-radius: 8px;
|
||||||
a {
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
text-decoration: none;
|
max-width: 760px;
|
||||||
color: #1e90ff;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
h1 { margin: 0 0 10px; }
|
||||||
|
.meta { color: #666; font-size: 14px; margin-bottom: 15px; }
|
||||||
|
.content { font-size: 16px; line-height: 1.6; }
|
||||||
|
.footer { margin-top: 20px; font-size: 13px; color: #999; }
|
||||||
|
img { max-width: 100%; border-radius: 4px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="container">
|
||||||
<h1>{{title}}</h1>
|
<h1>{{title}}</h1>
|
||||||
<p>{{description}}</p>
|
<div class="meta">作者:{{author}} | 时间:{{date}}</div>
|
||||||
<p><a href="{{link}}">{{link}}</a></p>
|
<div class="content">{{content}}</div>
|
||||||
|
<div class="footer">
|
||||||
|
来自 <a href="{{link}}" target="_blank">{{feedTitle}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -4,30 +4,29 @@ import puppeteer from 'puppeteer';
|
|||||||
|
|
||||||
const screenshot = {
|
const screenshot = {
|
||||||
/**
|
/**
|
||||||
* 调用浏览器截图
|
* rss网页截图
|
||||||
* @param title 标题
|
* @param feedItem 对象
|
||||||
* @param link 链接
|
* @param savePath 保存路径
|
||||||
* @param description 描述
|
* @returns {Promise<*>}
|
||||||
* @returns {Promise<string>} 图片所在目录
|
|
||||||
*/
|
*/
|
||||||
// TODO 待优化
|
async generateScreenshot(feedItem, savePath) {
|
||||||
async rssScreenshot(title, link, description = '') {
|
const htmlTemplate = fs.readFileSync(paths.rssHTML, 'utf-8');
|
||||||
const templatePath = paths.rssHTML;
|
const html = htmlTemplate
|
||||||
let html = fs.readFileSync(templatePath, 'utf8');
|
.replace('{{title}}', feedItem.title)
|
||||||
|
.replace('{{author}}', feedItem.author)
|
||||||
|
.replace('{{content}}', feedItem.content)
|
||||||
|
.replace('{{link}}', feedItem.link)
|
||||||
|
.replace('{{date}}', new Date(feedItem.date).toLocaleString())
|
||||||
|
.replace('{{feedTitle}}', feedItem.feedTitle)
|
||||||
|
.replace('{{image}}', feedItem.image || '');
|
||||||
|
|
||||||
html = html
|
const browser = await puppeteer.launch({ headers: 'new' });
|
||||||
.replace('{{title}}', title || '')
|
|
||||||
.replace('{{link}}', link || '')
|
|
||||||
.replace('{{description}}', description || '');
|
|
||||||
|
|
||||||
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setContent(html, { waitUntil: 'networkidle0' });
|
await page.setContent(html, { waitUntil: 'networkidle0' });
|
||||||
|
await page.setViewport({ width: 800, height: 600 });
|
||||||
const imagePath = `/tmp/rss_card_${Date.now()}.png`;
|
await page.screenshot({ path: savePath, fullPage: true });
|
||||||
await page.screenshot({ path: imagePath });
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
return imagePath;
|
return savePath;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
39
models/rss/rss.js
Normal file
39
models/rss/rss.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Parser from 'rss-parser';
|
||||||
|
|
||||||
|
const parser = new Parser();
|
||||||
|
//去掉不干净的东西
|
||||||
|
const cleanHTML = (html) => {
|
||||||
|
return html
|
||||||
|
.replace(/该渲染由.*?<\/blockquote>/gs, '')
|
||||||
|
.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
|
||||||
|
.replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const rssTools = {
|
||||||
|
/**
|
||||||
|
* 拉取rss
|
||||||
|
* 已适配Atom&RSS2.0
|
||||||
|
* @param url rss地址
|
||||||
|
* @returns {Promise<{title: *, link: *, content: *, author, date, feedTitle: string, feedLink: string, image}[]|null>}
|
||||||
|
*/
|
||||||
|
async fetchFeed(url) {
|
||||||
|
try {
|
||||||
|
const feed = await parser.parseURL(url);
|
||||||
|
return feed.items.map((item) => ({
|
||||||
|
title: item.title,
|
||||||
|
link: item.link,
|
||||||
|
content: cleanHTML(item['content:encoded'] || item.content || item.description || ''),
|
||||||
|
author: item.creator || item.author || feed.title,
|
||||||
|
date: item.pubDate || item.isoDate,
|
||||||
|
feedTitle: feed.title,
|
||||||
|
feedLink: feed.link,
|
||||||
|
image: feed.image?.url || '',
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`RSS 拉取失败: ${url}`, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default rssTools;
|
Loading…
x
Reference in New Issue
Block a user