@anyul/koishi-plugin-rss 5.2.2 → 5.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/error-handler.js +2 -5
- package/lib/commands/index.d.ts +17 -1
- package/lib/commands/index.js +388 -2
- package/lib/commands/subscription-edit.d.ts +7 -0
- package/lib/commands/subscription-edit.js +177 -0
- package/lib/commands/subscription-management.d.ts +12 -0
- package/lib/commands/subscription-management.js +176 -0
- package/lib/commands/utils.d.ts +13 -1
- package/lib/commands/utils.js +43 -2
- package/lib/config.js +19 -0
- package/lib/core/ai.d.ts +16 -2
- package/lib/core/ai.js +73 -6
- package/lib/core/feeder.d.ts +1 -1
- package/lib/core/feeder.js +238 -125
- package/lib/core/item-processor.d.ts +5 -0
- package/lib/core/item-processor.js +66 -136
- package/lib/core/notification-queue.d.ts +2 -0
- package/lib/core/notification-queue.js +80 -33
- package/lib/core/parser.js +12 -0
- package/lib/core/renderer.d.ts +15 -0
- package/lib/core/renderer.js +91 -23
- package/lib/index.js +28 -784
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +24 -0
- package/lib/utils/common.js +52 -3
- package/lib/utils/error-handler.d.ts +8 -0
- package/lib/utils/error-handler.js +27 -0
- package/lib/utils/error-tracker.js +24 -8
- package/lib/utils/fetcher.js +68 -9
- package/lib/utils/logger.d.ts +4 -2
- package/lib/utils/logger.js +144 -6
- package/lib/utils/media.js +3 -6
- package/lib/utils/sanitizer.d.ts +58 -0
- package/lib/utils/sanitizer.js +227 -0
- package/lib/utils/security.d.ts +75 -0
- package/lib/utils/security.js +312 -0
- package/lib/utils/structured-logger.js +3 -20
- package/package.json +2 -1
|
@@ -43,6 +43,8 @@ const renderer_1 = require("./renderer");
|
|
|
43
43
|
const template_1 = require("../utils/template");
|
|
44
44
|
const ai_1 = require("./ai");
|
|
45
45
|
const marked_1 = require("marked");
|
|
46
|
+
const sanitizer_1 = require("../utils/sanitizer");
|
|
47
|
+
const security_1 = require("../utils/security");
|
|
46
48
|
class RssItemProcessor {
|
|
47
49
|
ctx;
|
|
48
50
|
config;
|
|
@@ -59,6 +61,11 @@ class RssItemProcessor {
|
|
|
59
61
|
let html;
|
|
60
62
|
let videoList = [];
|
|
61
63
|
item.description = item.description?.join?.('') || item.description;
|
|
64
|
+
// HTML 安全清理
|
|
65
|
+
const sanitizer = (0, sanitizer_1.createSanitizer)(this.config);
|
|
66
|
+
if (sanitizer.isEnabled() && item.description) {
|
|
67
|
+
item.description = sanitizer.sanitize(item.description);
|
|
68
|
+
}
|
|
62
69
|
// --- AI 逻辑 START ---
|
|
63
70
|
let aiSummary = "";
|
|
64
71
|
let formattedAiSummary = "";
|
|
@@ -205,41 +212,7 @@ class RssItemProcessor {
|
|
|
205
212
|
await Promise.all(html('img').map(async (v, i) => i.attribs.src = await (0, media_1.getImageUrl)(this.ctx, this.config, this.$http, i.attribs.src, arg, true)).get());
|
|
206
213
|
}
|
|
207
214
|
html('img').attr('style', 'object-fit:scale-down;max-width:100%;');
|
|
208
|
-
let msg =
|
|
209
|
-
const imageMode = this.config.basic?.imageMode;
|
|
210
|
-
if (imageMode == 'base64') {
|
|
211
|
-
(0, logger_1.debug)(this.config, '使用 base64 模式渲染', 'render mode', 'info');
|
|
212
|
-
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, html.html(), arg)).toString();
|
|
213
|
-
}
|
|
214
|
-
else if (imageMode == 'File' || imageMode == 'assets') {
|
|
215
|
-
if (!this.ctx.puppeteer) {
|
|
216
|
-
(0, logger_1.debug)(this.config, '未安装 puppeteer 插件,跳过图片渲染', 'puppeteer error', 'error');
|
|
217
|
-
msg = html.html();
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
try {
|
|
221
|
-
(0, logger_1.debug)(this.config, `使用 ${imageMode} 模式渲染`, 'render mode', 'info');
|
|
222
|
-
let processedHtml = await (0, renderer_1.preprocessHtmlImages)(this.ctx, this.config, this.$http, html.html(), arg);
|
|
223
|
-
if ((this.config.template?.deviceScaleFactor ?? 1) !== 1) {
|
|
224
|
-
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, processedHtml, arg)).toString();
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
msg = await this.ctx.puppeteer.render(processedHtml);
|
|
228
|
-
}
|
|
229
|
-
msg = await (0, media_1.puppeteerToFile)(this.ctx, this.config, msg);
|
|
230
|
-
(0, logger_1.debug)(this.config, 'puppeteer 渲染完成', 'render success', 'info');
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
(0, logger_1.debug)(this.config, `puppeteer render 失败: ${error}`, 'puppeteer error', 'error');
|
|
234
|
-
msg = html.html();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
// 未知 imageMode,回退到 HTML
|
|
240
|
-
(0, logger_1.debug)(this.config, `未知的 imageMode: ${imageMode},回退到 HTML`, 'render warning', 'error');
|
|
241
|
-
msg = html.html();
|
|
242
|
-
}
|
|
215
|
+
let msg = await this.renderImage(html.html(), arg);
|
|
243
216
|
return parseContent(this.config.template?.customRemark || '', { ...item, arg, description: msg });
|
|
244
217
|
}
|
|
245
218
|
async processContentTemplate(item, arg, html, parseContent) {
|
|
@@ -300,42 +273,7 @@ class RssItemProcessor {
|
|
|
300
273
|
}
|
|
301
274
|
html('img').attr('style', 'object-fit:scale-down;max-width:100%;');
|
|
302
275
|
(0, logger_1.debug)(this.config, `当前 imageMode: ${this.config.basic?.imageMode}`, 'imageMode', 'info');
|
|
303
|
-
let msg =
|
|
304
|
-
const imageMode = this.config.basic?.imageMode;
|
|
305
|
-
if (imageMode == 'base64') {
|
|
306
|
-
(0, logger_1.debug)(this.config, '使用 base64 模式渲染', 'render mode', 'info');
|
|
307
|
-
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, html.html(), arg)).toString();
|
|
308
|
-
}
|
|
309
|
-
else if (imageMode == 'File' || imageMode == 'assets') {
|
|
310
|
-
if (!this.ctx.puppeteer) {
|
|
311
|
-
(0, logger_1.debug)(this.config, '未安装 puppeteer 插件,跳过图片渲染', 'puppeteer error', 'error');
|
|
312
|
-
msg = html.html();
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
try {
|
|
316
|
-
(0, logger_1.debug)(this.config, `使用 ${imageMode} 模式渲染`, 'render mode', 'info');
|
|
317
|
-
let processedHtml = await (0, renderer_1.preprocessHtmlImages)(this.ctx, this.config, this.$http, html.html(), arg);
|
|
318
|
-
if ((this.config.template?.deviceScaleFactor ?? 1) !== 1) {
|
|
319
|
-
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, processedHtml, arg)).toString();
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
msg = await this.ctx.puppeteer.render(processedHtml);
|
|
323
|
-
}
|
|
324
|
-
(0, logger_1.debug)(this.config, `puppeteer.render() 返回: ${msg.substring(0, 100)}...`, 'puppeteer result', 'info');
|
|
325
|
-
msg = await (0, media_1.puppeteerToFile)(this.ctx, this.config, msg);
|
|
326
|
-
(0, logger_1.debug)(this.config, `puppeteerToFile 转换完成`, 'puppeteer', 'info');
|
|
327
|
-
}
|
|
328
|
-
catch (error) {
|
|
329
|
-
(0, logger_1.debug)(this.config, `puppeteer render 失败: ${error}`, 'puppeteer error', 'error');
|
|
330
|
-
msg = html.html();
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
// 未知 imageMode,回退到 HTML
|
|
336
|
-
(0, logger_1.debug)(this.config, `未知的 imageMode: ${imageMode},回退到 HTML`, 'render warning', 'error');
|
|
337
|
-
msg = html.html();
|
|
338
|
-
}
|
|
276
|
+
let msg = await this.renderImage(html.html(), arg);
|
|
339
277
|
return msg;
|
|
340
278
|
}
|
|
341
279
|
async processOnlyDescriptionTemplate(item, arg, html, parseContent) {
|
|
@@ -369,82 +307,34 @@ class RssItemProcessor {
|
|
|
369
307
|
await Promise.all(html('img').map(async (v, i) => i.attribs.src = await (0, media_1.getImageUrl)(this.ctx, this.config, this.$http, i.attribs.src, arg, true)).get());
|
|
370
308
|
}
|
|
371
309
|
html('img').attr('style', 'object-fit:scale-down;max-width:100%;');
|
|
372
|
-
let msg =
|
|
373
|
-
const imageMode = this.config.basic?.imageMode;
|
|
374
|
-
if (imageMode == 'base64') {
|
|
375
|
-
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, html.html(), arg)).toString();
|
|
376
|
-
}
|
|
377
|
-
else if (imageMode == 'File' || imageMode == 'assets') {
|
|
378
|
-
if (!this.ctx.puppeteer) {
|
|
379
|
-
(0, logger_1.debug)(this.config, '未安装 puppeteer 插件,跳过图片渲染', 'puppeteer error', 'error');
|
|
380
|
-
msg = html.html();
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
try {
|
|
384
|
-
let processedHtml = await (0, renderer_1.preprocessHtmlImages)(this.ctx, this.config, this.$http, html.html(), arg);
|
|
385
|
-
if ((this.config.template?.deviceScaleFactor ?? 1) !== 1) {
|
|
386
|
-
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, processedHtml, arg)).toString();
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
msg = await this.ctx.puppeteer.render(processedHtml);
|
|
390
|
-
}
|
|
391
|
-
msg = await (0, media_1.puppeteerToFile)(this.ctx, this.config, msg);
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
(0, logger_1.debug)(this.config, `puppeteer render 失败: ${error}`, 'puppeteer error', 'error');
|
|
395
|
-
msg = html.html();
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
// 未知 imageMode,回退到 HTML
|
|
401
|
-
(0, logger_1.debug)(this.config, `未知的 imageMode: ${imageMode},回退到 HTML`, 'render warning', 'error');
|
|
402
|
-
msg = html.html();
|
|
403
|
-
}
|
|
310
|
+
let msg = await this.renderImage(html.html(), arg);
|
|
404
311
|
return msg;
|
|
405
312
|
}
|
|
406
313
|
async processLinkTemplate(item, arg) {
|
|
407
314
|
let html = cheerio.load(item.description);
|
|
408
315
|
let src = html('a')[0].attribs.href;
|
|
409
316
|
(0, logger_1.debug)(this.config, src, 'link src', 'info');
|
|
317
|
+
// URL 安全验证
|
|
318
|
+
try {
|
|
319
|
+
(0, security_1.validateUrlOrThrow)(src, (0, security_1.getSecurityOptions)(this.config));
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error instanceof security_1.SecurityError) {
|
|
323
|
+
(0, logger_1.debug)(this.config, `链接 URL 安全验证失败: ${error.message}`, 'security', 'error');
|
|
324
|
+
return `链接安全验证失败: ${error.message}`;
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
410
328
|
let html2 = cheerio.load((await this.$http(src, arg)).data);
|
|
411
329
|
if (arg?.proxyAgent?.enabled) {
|
|
412
330
|
await Promise.all(html2('img').map(async (v, i) => i.attribs.src = await (0, media_1.getImageUrl)(this.ctx, this.config, this.$http, i.attribs.src, arg, true)).get());
|
|
413
331
|
}
|
|
414
332
|
html2('img').attr('style', 'object-fit:scale-down;max-width:100%;');
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
else if (imageMode == 'File' || imageMode == 'assets') {
|
|
422
|
-
if (!this.ctx.puppeteer) {
|
|
423
|
-
(0, logger_1.debug)(this.config, '未安装 puppeteer 插件,跳过图片渲染', 'puppeteer error', 'error');
|
|
424
|
-
msg = html2.xml();
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
try {
|
|
428
|
-
let processedHtml = await (0, renderer_1.preprocessHtmlImages)(this.ctx, this.config, this.$http, html2.xml(), arg);
|
|
429
|
-
if ((this.config.template?.deviceScaleFactor ?? 1) !== 1) {
|
|
430
|
-
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, processedHtml, arg)).toString();
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
msg = await this.ctx.puppeteer.render(processedHtml);
|
|
434
|
-
}
|
|
435
|
-
msg = await (0, media_1.puppeteerToFile)(this.ctx, this.config, msg);
|
|
436
|
-
}
|
|
437
|
-
catch (error) {
|
|
438
|
-
(0, logger_1.debug)(this.config, `puppeteer render 失败: ${error}`, 'puppeteer error', 'error');
|
|
439
|
-
msg = html2.xml();
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
// 未知 imageMode,回退到 HTML
|
|
445
|
-
(0, logger_1.debug)(this.config, `未知的 imageMode: ${imageMode},回退到 HTML`, 'render warning', 'error');
|
|
446
|
-
msg = html2.xml();
|
|
447
|
-
}
|
|
333
|
+
// link 模板使用订阅级参数设置 body 样式
|
|
334
|
+
const bodyWidth = arg?.bodyWidth ?? this.config.template?.bodyWidth ?? 600;
|
|
335
|
+
const bodyPadding = arg?.bodyPadding ?? this.config.template?.bodyPadding ?? 20;
|
|
336
|
+
html2('body').attr('style', `width:${bodyWidth}px;padding:${bodyPadding}px;`);
|
|
337
|
+
let msg = await this.renderImage(html2.xml(), arg);
|
|
448
338
|
return msg;
|
|
449
339
|
}
|
|
450
340
|
async processVideos(html, arg, videoList) {
|
|
@@ -464,5 +354,45 @@ class RssItemProcessor {
|
|
|
464
354
|
return (0, koishi_1.h)('video', { src, poster });
|
|
465
355
|
}).join('');
|
|
466
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* 统一的图片渲染方法
|
|
359
|
+
* 提取了 custom、default、only description、link 模板中重复的图片渲染逻辑
|
|
360
|
+
*/
|
|
361
|
+
async renderImage(htmlContent, arg) {
|
|
362
|
+
const imageMode = this.config.basic?.imageMode;
|
|
363
|
+
// base64 模式
|
|
364
|
+
if (imageMode === 'base64') {
|
|
365
|
+
(0, logger_1.debug)(this.config, '使用 base64 模式渲染', 'render mode', 'info');
|
|
366
|
+
return (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, htmlContent, arg)).toString();
|
|
367
|
+
}
|
|
368
|
+
// File 或 assets 模式
|
|
369
|
+
if (imageMode === 'File' || imageMode === 'assets') {
|
|
370
|
+
if (!this.ctx.puppeteer) {
|
|
371
|
+
(0, logger_1.debug)(this.config, '未安装 puppeteer 插件,跳过图片渲染', 'puppeteer error', 'error');
|
|
372
|
+
return htmlContent;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
(0, logger_1.debug)(this.config, `使用 ${imageMode} 模式渲染`, 'render mode', 'info');
|
|
376
|
+
const processedHtml = await (0, renderer_1.preprocessHtmlImages)(this.ctx, this.config, this.$http, htmlContent, arg);
|
|
377
|
+
let msg;
|
|
378
|
+
if ((this.config.template?.deviceScaleFactor ?? 1) !== 1) {
|
|
379
|
+
msg = (await (0, renderer_1.renderHtml2Image)(this.ctx, this.config, this.$http, processedHtml, arg)).toString();
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
msg = await this.ctx.puppeteer.render(processedHtml);
|
|
383
|
+
}
|
|
384
|
+
msg = await (0, media_1.puppeteerToFile)(this.ctx, this.config, msg);
|
|
385
|
+
(0, logger_1.debug)(this.config, 'puppeteer 渲染完成', 'render success', 'info');
|
|
386
|
+
return msg;
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
(0, logger_1.debug)(this.config, `puppeteer render 失败: ${error}`, 'puppeteer error', 'error');
|
|
390
|
+
return htmlContent;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// 未知模式,回退到 HTML
|
|
394
|
+
(0, logger_1.debug)(this.config, `未知的 imageMode: ${imageMode},回退到 HTML`, 'render warning', 'error');
|
|
395
|
+
return htmlContent;
|
|
396
|
+
}
|
|
467
397
|
}
|
|
468
398
|
exports.RssItemProcessor = RssItemProcessor;
|
|
@@ -38,6 +38,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.NotificationQueueManager = void 0;
|
|
41
|
+
const error_handler_1 = require("../utils/error-handler");
|
|
42
|
+
const error_tracker_1 = require("../utils/error-tracker");
|
|
41
43
|
const logger_1 = require("../utils/logger");
|
|
42
44
|
/**
|
|
43
45
|
* 消息发送队列管理器
|
|
@@ -54,6 +56,26 @@ class NotificationQueueManager {
|
|
|
54
56
|
this.ctx = ctx;
|
|
55
57
|
this.config = config;
|
|
56
58
|
}
|
|
59
|
+
buildTaskLogContext(task) {
|
|
60
|
+
const context = {
|
|
61
|
+
subscribeId: task.subscribeId,
|
|
62
|
+
rssId: task.rssId,
|
|
63
|
+
uid: task.uid,
|
|
64
|
+
guildId: task.guildId,
|
|
65
|
+
platform: task.platform,
|
|
66
|
+
retryCount: task.retryCount,
|
|
67
|
+
};
|
|
68
|
+
if (task.id !== undefined) {
|
|
69
|
+
context.taskId = String(task.id);
|
|
70
|
+
}
|
|
71
|
+
if (task.platform && task.guildId) {
|
|
72
|
+
context.target = `${task.platform}:${task.guildId}`;
|
|
73
|
+
}
|
|
74
|
+
return context;
|
|
75
|
+
}
|
|
76
|
+
createTaskDebug(task) {
|
|
77
|
+
return (0, logger_1.createDebugWithContext)(this.config, this.buildTaskLogContext(task));
|
|
78
|
+
}
|
|
57
79
|
/**
|
|
58
80
|
* 添加任务到队列
|
|
59
81
|
*/
|
|
@@ -65,8 +87,9 @@ class NotificationQueueManager {
|
|
|
65
87
|
createdAt: new Date(),
|
|
66
88
|
updatedAt: new Date()
|
|
67
89
|
};
|
|
90
|
+
const taskDebug = this.createTaskDebug(queueTask);
|
|
68
91
|
await this.ctx.database.create('rss_notification_queue', queueTask);
|
|
69
|
-
(
|
|
92
|
+
taskDebug(`任务已加入队列: [${task.rssId}] ${task.content.title}`, 'queue', 'info');
|
|
70
93
|
return queueTask;
|
|
71
94
|
}
|
|
72
95
|
/**
|
|
@@ -74,7 +97,7 @@ class NotificationQueueManager {
|
|
|
74
97
|
*/
|
|
75
98
|
async processQueue() {
|
|
76
99
|
if (this.processing) {
|
|
77
|
-
(0, logger_1.debug)(this.config, '队列正在处理中,跳过本次', 'queue', 'details');
|
|
100
|
+
(0, logger_1.debug)(this.config, '队列正在处理中,跳过本次', 'queue', 'details', { processing: true });
|
|
78
101
|
return;
|
|
79
102
|
}
|
|
80
103
|
this.processing = true;
|
|
@@ -84,15 +107,16 @@ class NotificationQueueManager {
|
|
|
84
107
|
if (tasks.length === 0) {
|
|
85
108
|
return;
|
|
86
109
|
}
|
|
87
|
-
(0, logger_1.debug)(this.config, `开始处理 ${tasks.length} 个待发送任务`, 'queue', 'info');
|
|
110
|
+
(0, logger_1.debug)(this.config, `开始处理 ${tasks.length} 个待发送任务`, 'queue', 'info', { taskCount: tasks.length });
|
|
88
111
|
// 2. 逐个处理任务
|
|
89
112
|
for (const task of tasks) {
|
|
90
113
|
await this.processTask(task);
|
|
91
114
|
}
|
|
92
115
|
}
|
|
93
116
|
catch (err) {
|
|
94
|
-
(0,
|
|
95
|
-
|
|
117
|
+
const normalizedError = (0, error_handler_1.normalizeError)(err);
|
|
118
|
+
(0, logger_1.debug)(this.config, `队列处理异常: ${normalizedError.message}`, 'queue', 'error', { processing: true });
|
|
119
|
+
(0, error_tracker_1.trackError)(normalizedError, { operation: 'processQueue' });
|
|
96
120
|
}
|
|
97
121
|
finally {
|
|
98
122
|
this.processing = false;
|
|
@@ -118,13 +142,14 @@ class NotificationQueueManager {
|
|
|
118
142
|
* 处理单个任务
|
|
119
143
|
*/
|
|
120
144
|
async processTask(task) {
|
|
121
|
-
|
|
145
|
+
const taskDebug = this.createTaskDebug(task);
|
|
146
|
+
taskDebug(`处理任务 [${task.rssId}] ${task.content.title} (重试${task.retryCount}次)`, 'queue', 'details');
|
|
122
147
|
try {
|
|
123
148
|
// 尝试发送消息
|
|
124
149
|
await this.sendMessage(task);
|
|
125
150
|
// 发送成功:标记为 SUCCESS
|
|
126
151
|
await this.markTaskSuccess(task.id);
|
|
127
|
-
(
|
|
152
|
+
taskDebug(`✓ 任务发送成功: [${task.rssId}] ${task.content.title}`, 'queue', 'info');
|
|
128
153
|
// 写入缓存
|
|
129
154
|
await this.cacheMessage(task);
|
|
130
155
|
}
|
|
@@ -139,16 +164,17 @@ class NotificationQueueManager {
|
|
|
139
164
|
async sendMessage(task) {
|
|
140
165
|
const { guildId, platform, content } = task;
|
|
141
166
|
const target = `${platform}:${guildId}`;
|
|
167
|
+
const taskDebug = this.createTaskDebug(task);
|
|
142
168
|
try {
|
|
143
169
|
// 第一次尝试:发送原始消息
|
|
144
170
|
await this.ctx.broadcast([target], content.message);
|
|
145
|
-
(
|
|
171
|
+
taskDebug(`消息发送成功: ${target}`, 'queue', 'details');
|
|
146
172
|
}
|
|
147
173
|
catch (sendError) {
|
|
148
174
|
// OneBot retcode 1200: 不支持的消息格式(通常是视频)
|
|
149
175
|
const isOneBot1200 = sendError.code?.toString?.() === '1200' || sendError.message?.includes('1200');
|
|
150
176
|
if (isOneBot1200 && !content.isDowngraded) {
|
|
151
|
-
(
|
|
177
|
+
taskDebug(`检测到 OneBot 1200 错误,尝试降级处理`, 'queue', 'info', { errorCode: '1200' });
|
|
152
178
|
throw { ...sendError, isMediaError: true, requiresDowngrade: true };
|
|
153
179
|
}
|
|
154
180
|
throw sendError;
|
|
@@ -158,25 +184,40 @@ class NotificationQueueManager {
|
|
|
158
184
|
* 处理发送错误
|
|
159
185
|
*/
|
|
160
186
|
async handleSendError(task, error) {
|
|
161
|
-
const
|
|
187
|
+
const taskDebug = this.createTaskDebug(task);
|
|
188
|
+
const normalizedError = (0, error_handler_1.normalizeError)(error);
|
|
189
|
+
const errorMsg = normalizedError.message || 'Unknown error';
|
|
190
|
+
(0, error_tracker_1.trackError)(normalizedError, {
|
|
191
|
+
...this.buildTaskLogContext(task),
|
|
192
|
+
failReason: errorMsg,
|
|
193
|
+
requiresDowngrade: Boolean(error?.requiresDowngrade),
|
|
194
|
+
});
|
|
162
195
|
// 1. 永久性错误 (Fatal) - 不需要重试
|
|
163
196
|
if (this.isFatalError(error)) {
|
|
164
197
|
await this.markTaskFailed(task.id, errorMsg);
|
|
165
|
-
(
|
|
198
|
+
taskDebug(`✗ 永久性失败,放弃重试: [${task.rssId}] ${task.content.title} - ${errorMsg}`, 'queue', 'error', {
|
|
199
|
+
fatal: true,
|
|
200
|
+
failReason: errorMsg,
|
|
201
|
+
});
|
|
166
202
|
return;
|
|
167
203
|
}
|
|
168
204
|
// 2. 降级重试 (Downgrade) - 针对媒体格式错误
|
|
169
205
|
if (error.requiresDowngrade && !task.content.isDowngraded) {
|
|
170
206
|
const downgradedContent = await this.downgradeMessage(task.content);
|
|
171
|
-
await this.updateTaskForDowngrade(task
|
|
172
|
-
(
|
|
207
|
+
await this.updateTaskForDowngrade(task, downgradedContent);
|
|
208
|
+
taskDebug(`→ 消息已降级,立即重试: [${task.rssId}] ${task.content.title}`, 'queue', 'info', {
|
|
209
|
+
requiresDowngrade: true,
|
|
210
|
+
});
|
|
173
211
|
return;
|
|
174
212
|
}
|
|
175
213
|
// 3. 暂时性错误 (Transient) - 使用指数退避
|
|
176
214
|
const delay = this.backoffDelays[task.retryCount] || this.backoffDelays[this.backoffDelays.length - 1];
|
|
177
215
|
const nextTime = new Date(Date.now() + delay * 1000);
|
|
178
|
-
await this.markTaskRetry(task
|
|
179
|
-
(
|
|
216
|
+
await this.markTaskRetry(task, nextTime, errorMsg);
|
|
217
|
+
taskDebug(`→ 任务将在 ${Math.ceil(delay / 60)} 分钟后重试: [${task.rssId}] ${task.content.title}`, 'queue', 'info', {
|
|
218
|
+
nextRetryTime: nextTime.toISOString(),
|
|
219
|
+
failReason: errorMsg,
|
|
220
|
+
});
|
|
180
221
|
}
|
|
181
222
|
/**
|
|
182
223
|
* 判断是否为永久性错误
|
|
@@ -239,13 +280,11 @@ class NotificationQueueManager {
|
|
|
239
280
|
/**
|
|
240
281
|
* 标记任务为重试
|
|
241
282
|
*/
|
|
242
|
-
async markTaskRetry(
|
|
243
|
-
await this.ctx.database.set('rss_notification_queue', { id:
|
|
283
|
+
async markTaskRetry(task, nextTime, reason) {
|
|
284
|
+
await this.ctx.database.set('rss_notification_queue', { id: task.id }, {
|
|
244
285
|
status: 'RETRY',
|
|
245
286
|
nextRetryTime: nextTime,
|
|
246
|
-
retryCount:
|
|
247
|
-
return (tasks[0]?.retryCount || 0) + 1;
|
|
248
|
-
}),
|
|
287
|
+
retryCount: (task.retryCount || 0) + 1,
|
|
249
288
|
failReason: reason,
|
|
250
289
|
updatedAt: new Date()
|
|
251
290
|
});
|
|
@@ -253,17 +292,12 @@ class NotificationQueueManager {
|
|
|
253
292
|
/**
|
|
254
293
|
* 更新任务为降级重试
|
|
255
294
|
*/
|
|
256
|
-
async updateTaskForDowngrade(
|
|
257
|
-
|
|
258
|
-
const tasks = await this.ctx.database.get('rss_notification_queue', { id: taskId });
|
|
259
|
-
if (tasks.length === 0)
|
|
260
|
-
return;
|
|
261
|
-
const currentTask = tasks[0];
|
|
262
|
-
await this.ctx.database.set('rss_notification_queue', { id: taskId }, {
|
|
295
|
+
async updateTaskForDowngrade(task, newContent) {
|
|
296
|
+
await this.ctx.database.set('rss_notification_queue', { id: task.id }, {
|
|
263
297
|
content: newContent,
|
|
264
298
|
status: 'RETRY',
|
|
265
299
|
nextRetryTime: new Date(), // 立即重试
|
|
266
|
-
retryCount:
|
|
300
|
+
retryCount: (task.retryCount || 0) + 1,
|
|
267
301
|
updatedAt: new Date()
|
|
268
302
|
});
|
|
269
303
|
}
|
|
@@ -284,6 +318,7 @@ class NotificationQueueManager {
|
|
|
284
318
|
if (!this.config.cache?.enabled) {
|
|
285
319
|
return;
|
|
286
320
|
}
|
|
321
|
+
const taskDebug = this.createTaskDebug(task);
|
|
287
322
|
const { getMessageCache } = await Promise.resolve().then(() => __importStar(require('../utils/message-cache')));
|
|
288
323
|
const cache = getMessageCache();
|
|
289
324
|
if (!cache) {
|
|
@@ -304,7 +339,12 @@ class NotificationQueueManager {
|
|
|
304
339
|
});
|
|
305
340
|
}
|
|
306
341
|
catch (err) {
|
|
307
|
-
(0,
|
|
342
|
+
const normalizedError = (0, error_handler_1.normalizeError)(err);
|
|
343
|
+
taskDebug(`缓存消息失败: ${normalizedError.message}`, 'cache', 'info');
|
|
344
|
+
(0, error_tracker_1.trackError)(normalizedError, {
|
|
345
|
+
...this.buildTaskLogContext(task),
|
|
346
|
+
operation: 'cacheMessage',
|
|
347
|
+
});
|
|
308
348
|
}
|
|
309
349
|
}
|
|
310
350
|
/**
|
|
@@ -325,7 +365,8 @@ class NotificationQueueManager {
|
|
|
325
365
|
async retryFailedTasks(taskId) {
|
|
326
366
|
const where = taskId ? { id: taskId } : { status: 'FAILED' };
|
|
327
367
|
const tasks = await this.ctx.database.get('rss_notification_queue', where);
|
|
328
|
-
|
|
368
|
+
const failedTasks = tasks.filter(task => task.status === 'FAILED');
|
|
369
|
+
for (const task of failedTasks) {
|
|
329
370
|
await this.ctx.database.set('rss_notification_queue', { id: task.id }, {
|
|
330
371
|
status: 'PENDING',
|
|
331
372
|
retryCount: 0,
|
|
@@ -333,8 +374,11 @@ class NotificationQueueManager {
|
|
|
333
374
|
updatedAt: new Date()
|
|
334
375
|
});
|
|
335
376
|
}
|
|
336
|
-
(0, logger_1.debug)(this.config, `已重置 ${
|
|
337
|
-
|
|
377
|
+
(0, logger_1.debug)(this.config, `已重置 ${failedTasks.length} 个失败任务为 PENDING 状态`, 'queue', 'info', {
|
|
378
|
+
resetCount: failedTasks.length,
|
|
379
|
+
taskId,
|
|
380
|
+
});
|
|
381
|
+
return failedTasks.length;
|
|
338
382
|
}
|
|
339
383
|
/**
|
|
340
384
|
* 清理旧的成功任务
|
|
@@ -345,7 +389,10 @@ class NotificationQueueManager {
|
|
|
345
389
|
for (const task of tasks) {
|
|
346
390
|
await this.ctx.database.remove('rss_notification_queue', { id: task.id });
|
|
347
391
|
}
|
|
348
|
-
(0, logger_1.debug)(this.config, `已清理 ${tasks.length} 个旧的成功任务`, 'queue', 'info'
|
|
392
|
+
(0, logger_1.debug)(this.config, `已清理 ${tasks.length} 个旧的成功任务`, 'queue', 'info', {
|
|
393
|
+
cleanupCount: tasks.length,
|
|
394
|
+
olderThanHours,
|
|
395
|
+
});
|
|
349
396
|
return tasks.length;
|
|
350
397
|
}
|
|
351
398
|
}
|
package/lib/core/parser.js
CHANGED
|
@@ -37,10 +37,22 @@ exports.getRssData = getRssData;
|
|
|
37
37
|
const cheerio = __importStar(require("cheerio"));
|
|
38
38
|
const logger_1 = require("../utils/logger");
|
|
39
39
|
const common_1 = require("../utils/common");
|
|
40
|
+
const security_1 = require("../utils/security");
|
|
40
41
|
const X2JS = require("x2js");
|
|
41
42
|
const x2js = new X2JS();
|
|
42
43
|
async function getRssData(ctx, config, $http, url, arg) {
|
|
43
44
|
try {
|
|
45
|
+
// URL 安全验证
|
|
46
|
+
try {
|
|
47
|
+
(0, security_1.validateUrlOrThrow)(url, (0, security_1.getSecurityOptions)(config));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof security_1.SecurityError) {
|
|
51
|
+
(0, logger_1.debug)(config, `URL 安全验证失败: ${error.message}`, 'security', 'error');
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
44
56
|
// --- HTML 抓取预处理 START ---
|
|
45
57
|
let rssData;
|
|
46
58
|
let contentType = '';
|
package/lib/core/renderer.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
import { Config, rssArg } from '../types';
|
|
3
|
+
export interface RenderContentMetrics {
|
|
4
|
+
bodyScrollHeight: number;
|
|
5
|
+
bodyOffsetHeight: number;
|
|
6
|
+
documentScrollHeight: number;
|
|
7
|
+
contentRangeHeight: number;
|
|
8
|
+
maxElementBottom: number;
|
|
9
|
+
paddingTop: number;
|
|
10
|
+
paddingBottom: number;
|
|
11
|
+
marginTop: number;
|
|
12
|
+
marginBottom: number;
|
|
13
|
+
marginLeft: number;
|
|
14
|
+
bodyWidth: number;
|
|
15
|
+
viewportHeight: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function calculateContentHeight(metrics: RenderContentMetrics): number;
|
|
3
18
|
export declare function preprocessHtmlImages(ctx: Context, config: Config, $http: any, htmlContent: string, arg?: rssArg): Promise<string>;
|
|
4
19
|
export declare function renderHtml2Image(ctx: Context, config: Config, $http: any, htmlContent: string, arg?: rssArg): Promise<any>;
|