@anyul/koishi-plugin-rss 5.0.2 → 5.0.4

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.
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.findRssItem = findRssItem;
4
37
  exports.getLastContent = getLastContent;
@@ -10,12 +43,19 @@ exports.stopFeeder = stopFeeder;
10
43
  const koishi_1 = require("koishi");
11
44
  const logger_1 = require("../utils/logger");
12
45
  const common_1 = require("../utils/common");
13
- const media_1 = require("../utils/media");
14
46
  const parser_1 = require("./parser");
15
47
  const constants_1 = require("../constants");
16
- const message_cache_1 = require("../utils/message-cache");
17
48
  let interval = null;
49
+ let queueInterval = null;
18
50
  function findRssItem(rssList, keyword) {
51
+ // 优先匹配列表索引(用户看到的序号 1, 2, 3...)
52
+ if (typeof keyword === 'number' || /^\d+$/.test(String(keyword))) {
53
+ const listIndex = parseInt(String(keyword)) - 1; // 转换为数组索引(0-based)
54
+ if (listIndex >= 0 && listIndex < rssList.length) {
55
+ return rssList[listIndex];
56
+ }
57
+ }
58
+ // 其他匹配方式:按 rssId、url、title 等
19
59
  let index = ((rssList.findIndex(i => i.rssId === +keyword) + 1) ||
20
60
  (rssList.findIndex(i => i.url == keyword) + 1) ||
21
61
  (rssList.findIndex(i => i.url.indexOf(keyword) + 1) + 1) ||
@@ -28,13 +68,23 @@ function getLastContent(item, config) {
28
68
  return { ...obj, description: String(obj?.description).replaceAll(/\s/g, '') };
29
69
  }
30
70
  function formatArg(options, config) {
31
- let { arg, template } = options;
71
+ let { arg, template, auth } = options;
72
+ // 特殊处理:提取完整的 proxyAgent URL
73
+ let proxyAgentUrl;
74
+ if (arg && arg.includes('proxyAgent:')) {
75
+ const match = arg.match(/proxyAgent:([^,]+)/);
76
+ if (match) {
77
+ proxyAgentUrl = match[1];
78
+ // 从 arg 中移除 proxyAgent,避免被 split(":") 破坏
79
+ arg = arg.replace(/proxyAgent:[^,]+/, '').replace(/^,|,$/g, '').replace(/,,/g, ',');
80
+ }
81
+ }
32
82
  let json = Object.assign({}, ...(arg?.split(',')?.map((i) => ({ [i.split(":")[0]]: i.split(":")[1] })) || []));
33
- let key = ["forceLength", "reverse", "timeout", "interval", "merge", "maxRssItem", "firstLoad", "bodyWidth", "bodyPadding", "proxyAgent", "auth"];
83
+ let key = ["forceLength", "reverse", "timeout", "interval", "merge", "maxRssItem", "firstLoad", "bodyWidth", "bodyPadding", "filter", "block"];
34
84
  let booleanKey = ['firstLoad', "reverse", 'merge'];
35
85
  let numberKey = ['forceLength', "timeout", 'interval', 'maxRssItem', 'bodyWidth', 'bodyPadding'];
36
86
  let falseContent = ['false', 'null', ''];
37
- json = Object.assign({}, ...Object.keys(json).filter((i) => key.some((key) => key == i)).map((key) => ({ [key]: booleanKey.some((bkey) => bkey == key) ? falseContent.some((c) => c == json[key]) : numberKey.some((nkey) => nkey == key) ? (+json[key]) : json[key] })));
87
+ json = Object.assign({}, ...Object.keys(json).filter((i) => key.some((key) => key == i)).map((key) => ({ [key]: booleanKey.some((bkey) => bkey == key) ? !falseContent.some((c) => c == json[key]) : numberKey.some((nkey) => nkey == key) ? (+json[key]) : json[key] })));
38
88
  if (template && config.template) {
39
89
  json['template'] = template;
40
90
  }
@@ -48,25 +98,26 @@ function formatArg(options, config) {
48
98
  json.filter = json.filter.split("/");
49
99
  if (json.block && typeof json.block === 'string')
50
100
  json.block = json.block.split("/");
51
- // Proxy Argument Parsing
52
- if (json.proxyAgent) {
53
- if (['false', 'none', ''].includes(String(json.proxyAgent))) {
101
+ // Proxy Argument Parsing (使用提取的完整 URL)
102
+ if (proxyAgentUrl) {
103
+ if (['false', 'none', ''].includes(String(proxyAgentUrl))) {
54
104
  json.proxyAgent = { enabled: false };
55
105
  }
56
- else if (typeof json.proxyAgent === 'string') {
106
+ else if (typeof proxyAgentUrl === 'string') {
57
107
  // Parse string proxy: socks5://127.0.0.1:7890
58
- let protocolMatch = json.proxyAgent.match(/^(http|https|socks5)/);
108
+ let protocolMatch = proxyAgentUrl.match(/^(http|https|socks5)/);
59
109
  let protocol = protocolMatch ? protocolMatch[1] : 'http';
60
- let hostMatch = json.proxyAgent.match(/:\/\/([^:\/]+)/);
110
+ let hostMatch = proxyAgentUrl.match(/:\/\/([^:\/]+)/);
61
111
  let host = hostMatch ? hostMatch[1] : '';
62
- let portMatch = json.proxyAgent.match(/:(\d+)/);
112
+ let portMatch = proxyAgentUrl.match(/:(\d+)/);
63
113
  let port = portMatch ? parseInt(portMatch[1]) : 7890;
64
- let proxyAgent = { enabled: true, protocol, host, port };
65
- if (json.auth) {
66
- let [username, password] = json.auth.split("/");
67
- proxyAgent.auth = { username, password };
114
+ let proxyAgentObj = { enabled: true, protocol, host, port };
115
+ // Use auth from options if provided
116
+ if (auth) {
117
+ let [username, password] = auth.split("/");
118
+ proxyAgentObj.auth = { username, password };
68
119
  }
69
- json.proxyAgent = proxyAgent;
120
+ json.proxyAgent = proxyAgentObj;
70
121
  }
71
122
  }
72
123
  return json;
@@ -148,9 +199,11 @@ function mixinArg(arg, config) {
148
199
  (0, logger_1.debug)(config, `[DEBUG_PROXY] mixinArg return: ${JSON.stringify(res.proxyAgent)}`, 'mixin', 'details');
149
200
  return res;
150
201
  }
202
+ /**
203
+ * 生产者:抓取 RSS,发现新消息,存入队列
204
+ */
151
205
  async function feeder(deps, processor) {
152
- const { ctx, config, $http } = deps;
153
- // debug(config, "feeder run", 'debug');
206
+ const { ctx, config, $http, queueManager } = deps;
154
207
  // Use type assertion for custom table
155
208
  const rssList = await ctx.database.get('rssOwl', {});
156
209
  if (!rssList || rssList.length === 0)
@@ -275,8 +328,9 @@ async function feeder(deps, processor) {
275
328
  }
276
329
  (0, logger_1.debug)(config, `${rssItem.title}: Found ${rssItemArray.length} new items`, 'feeder', 'info');
277
330
  (0, logger_1.debug)(config, rssItemArray.map(i => i.title), '', 'info');
278
- // 6. Process Items (Generate Messages)
331
+ // 6. 生成消息并添加到队列(生产者核心逻辑)
279
332
  const itemsToSend = [...rssItemArray].reverse();
333
+ // 生成所有消息
280
334
  const messageList = (await Promise.all(itemsToSend.map(async (i) => await processor.parseRssItem(i, { ...rssItem, ...arg }, rssItem.author)))).filter(m => m); // Filter empty messages
281
335
  if (messageList.length === 0) {
282
336
  (0, logger_1.debug)(config, `${rssItem.title}: Items found but parsed to empty messages`, 'feeder', 'info');
@@ -284,7 +338,7 @@ async function feeder(deps, processor) {
284
338
  await ctx.database.set('rssOwl', { id: rssItem.id }, { lastPubDate, arg: originalArg, lastContent: { itemArray: currentContent } });
285
339
  continue;
286
340
  }
287
- // 7. Construct Final Message
341
+ // 7. 构建最终消息
288
342
  let message = "";
289
343
  const shouldMerge = arg.merge === true || config.basic?.merge === '一直合并' || (config.basic?.merge === '有多条更新时合并' && messageList.length > 1);
290
344
  // Check for video merge requirement
@@ -300,78 +354,28 @@ async function feeder(deps, processor) {
300
354
  const mentions = rssItem.followers.map((id) => `<at ${id === 'all' ? 'type="all"' : `id="${id}"`}/>`).join(" ");
301
355
  message += `<message>${mentions}</message>`;
302
356
  }
303
- // 8. Send Broadcast
304
- try {
305
- (0, logger_1.debug)(config, `Sending update for ${rssItem.title} to ${rssItem.platform}:${rssItem.guildId}`, 'feeder', 'details');
306
- // Koishi broadcast 会自动查找可用的 bot,无需手动检查
307
- // author 字段兼容用户ID和bot selfId两种格式
308
- // 发送消息
309
- try {
310
- await ctx.broadcast([`${rssItem.platform}:${rssItem.guildId}`], message);
311
- (0, logger_1.debug)(config, `更新成功:${rssItem.title}`, '', 'info');
312
- }
313
- catch (sendError) {
314
- // OneBot retcode 1200: 不支持的消息格式(通常是视频)
315
- if (sendError.code?.toString?.() === '1200' || sendError.message?.includes('1200')) {
316
- (0, logger_1.debug)(config, `消息格式不被支持,尝试清理视频元素后重试: ${rssItem.title}`, 'feeder', 'info');
317
- // 移除 video 元素,保留视频链接
318
- const fallbackMessage = message
319
- .replace(/<video[^>]*>.*?<\/video>/gis, (match) => {
320
- // 提取视频 URL
321
- const srcMatch = match.match(/src=["']([^"']+)["']/);
322
- if (srcMatch) {
323
- return `\n🎬 视频: ${srcMatch[1]}\n`;
324
- }
325
- return '\n[视频不支持]\n';
326
- });
327
- try {
328
- await ctx.broadcast([`${rssItem.platform}:${rssItem.guildId}`], fallbackMessage);
329
- (0, logger_1.debug)(config, `降级发送成功:${rssItem.title}`, '', 'info');
330
- }
331
- catch (retryError) {
332
- (0, logger_1.debug)(config, `降级发送也失败: ${retryError.message}`, 'feeder', 'error');
333
- throw retryError;
334
- }
335
- }
336
- else {
337
- throw sendError;
338
- }
339
- }
340
- // 缓存最终发送的消息
341
- if (config.cache?.enabled && messageList.length > 0) {
342
- const cache = (0, message_cache_1.getMessageCache)();
343
- if (cache) {
344
- // 缓存每条消息的最终形式
345
- for (let i = 0; i < itemsToSend.length && i < messageList.length; i++) {
346
- const item = itemsToSend[i];
347
- const finalMsg = messageList[i];
348
- try {
349
- await cache.addMessage({
350
- rssId: rssItem.rssId.toString(),
351
- guildId: rssItem.guildId,
352
- platform: rssItem.platform,
353
- title: item.title || '',
354
- content: item.description || '',
355
- link: item.link || '',
356
- pubDate: (0, common_1.parsePubDate)(config, item.pubDate),
357
- imageUrl: item.enclosure?.url || '',
358
- videoUrl: '',
359
- finalMessage: finalMsg // 缓存最终发送的消息
360
- });
361
- }
362
- catch (err) {
363
- (0, logger_1.debug)(config, `缓存消息失败: ${err.message}`, 'cache', 'info');
364
- }
365
- }
366
- }
367
- }
368
- }
369
- catch (err) {
370
- (0, logger_1.debug)(config, `RSS推送失败 [${rssItem.title}]: ${err.message}`, 'feeder', 'error');
371
- console.error(`RSS推送失败 [${rssItem.title}]: ${err.message}`);
372
- // 即使发送失败,也要更新数据库状态,避免无限重试
373
- }
374
- // 9. Update Database State
357
+ // 8. 添加任务到队列(关键变更:不再直接发送)
358
+ const taskContent = {
359
+ message,
360
+ originalItem: itemsToSend[0],
361
+ isDowngraded: false,
362
+ title: itemsToSend[0]?.title,
363
+ description: itemsToSend[0]?.description,
364
+ link: itemsToSend[0]?.link,
365
+ pubDate: (0, common_1.parsePubDate)(config, itemsToSend[0]?.pubDate),
366
+ imageUrl: itemsToSend[0]?.enclosure?.url
367
+ };
368
+ await queueManager.addTask({
369
+ subscribeId: String(rssItem.id),
370
+ rssId: rssItem.rssId || rssItem.title,
371
+ uid: itemsToSend[0]?.link || itemsToSend[0]?.guid || `${Date.now()}`,
372
+ guildId: rssItem.guildId,
373
+ platform: rssItem.platform,
374
+ content: taskContent
375
+ });
376
+ (0, logger_1.debug)(config, `✓ 已添加到发送队列: ${rssItem.title}`, 'feeder', 'info');
377
+ // 9. 更新数据库状态(关键:无论发送是否成功,都更新 lastPubDate)
378
+ // 这样即使 Bot 掉线,重启后也不会重复发送旧消息
375
379
  await ctx.database.set('rssOwl', { id: rssItem.id }, {
376
380
  lastPubDate,
377
381
  arg: originalArg,
@@ -383,21 +387,35 @@ async function feeder(deps, processor) {
383
387
  }
384
388
  }
385
389
  }
386
- function startFeeder(ctx, config, $http, processor) {
387
- const deps = { ctx, config, $http };
390
+ function startFeeder(ctx, config, $http, processor, queueManager) {
391
+ const deps = { ctx, config, $http, queueManager };
388
392
  // Initial run
389
393
  feeder(deps, processor).catch(err => console.error("Initial feeder run failed:", err));
394
+ // 启动生产者定时器(抓取 RSS)
390
395
  const refreshInterval = (config.basic?.refresh || 600) * 1000;
391
396
  interval = setInterval(async () => {
392
397
  if (config.basic?.imageMode === 'File') {
393
- await (0, media_1.delCache)(config);
398
+ const { delCache } = await Promise.resolve().then(() => __importStar(require('../utils/media')));
399
+ await delCache(config);
394
400
  }
395
401
  await feeder(deps, processor);
396
402
  }, refreshInterval);
403
+ // 启动消费者定时器(处理发送队列)
404
+ // 频率更高,确保消息快速发送
405
+ const queueProcessInterval = 30 * 1000; // 每 30 秒处理一次队列
406
+ queueInterval = setInterval(async () => {
407
+ await queueManager.processQueue();
408
+ }, queueProcessInterval);
409
+ // 立即处理一次队列(启动时)
410
+ queueManager.processQueue().catch(err => console.error("Initial queue processing failed:", err));
397
411
  }
398
412
  function stopFeeder() {
399
413
  if (interval) {
400
414
  clearInterval(interval);
401
415
  interval = null;
402
416
  }
417
+ if (queueInterval) {
418
+ clearInterval(queueInterval);
419
+ queueInterval = null;
420
+ }
403
421
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * 消息发送队列管理器
3
+ * 实现可靠的消息推送,支持重试、降级和错误处理
4
+ */
5
+ import { Context } from 'koishi';
6
+ import { Config } from '../types';
7
+ export type QueueStatus = 'PENDING' | 'RETRY' | 'FAILED' | 'SUCCESS';
8
+ /**
9
+ * 队列任务接口
10
+ */
11
+ export interface QueueTask {
12
+ id?: number;
13
+ subscribeId: string;
14
+ rssId: string;
15
+ uid: string;
16
+ guildId: string;
17
+ platform: string;
18
+ content: QueueTaskContent;
19
+ status: QueueStatus;
20
+ retryCount: number;
21
+ nextRetryTime?: Date;
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ failReason?: string;
25
+ }
26
+ /**
27
+ * 队列任务内容
28
+ */
29
+ export interface QueueTaskContent {
30
+ message: string;
31
+ originalItem?: any;
32
+ isDowngraded?: boolean;
33
+ title?: string;
34
+ description?: string;
35
+ link?: string;
36
+ pubDate?: Date;
37
+ imageUrl?: string;
38
+ }
39
+ /**
40
+ * 消息发送队列管理器
41
+ */
42
+ export declare class NotificationQueueManager {
43
+ private ctx;
44
+ private config;
45
+ private processing;
46
+ private maxRetries;
47
+ private batchSize;
48
+ private backoffDelays;
49
+ constructor(ctx: Context, config: Config);
50
+ /**
51
+ * 添加任务到队列
52
+ */
53
+ addTask(task: Omit<QueueTask, 'id' | 'status' | 'retryCount' | 'createdAt' | 'updatedAt'>): Promise<QueueTask>;
54
+ /**
55
+ * 处理队列中的任务
56
+ */
57
+ processQueue(): Promise<void>;
58
+ /**
59
+ * 获取待处理任务
60
+ */
61
+ private getPendingTasks;
62
+ /**
63
+ * 处理单个任务
64
+ */
65
+ private processTask;
66
+ /**
67
+ * 发送消息(带降级机制)
68
+ */
69
+ private sendMessage;
70
+ /**
71
+ * 处理发送错误
72
+ */
73
+ private handleSendError;
74
+ /**
75
+ * 判断是否为永久性错误
76
+ */
77
+ private isFatalError;
78
+ /**
79
+ * 降级消息(移除媒体元素)
80
+ */
81
+ private downgradeMessage;
82
+ /**
83
+ * 标记任务为成功
84
+ */
85
+ private markTaskSuccess;
86
+ /**
87
+ * 标记任务为重试
88
+ */
89
+ private markTaskRetry;
90
+ /**
91
+ * 更新任务为降级重试
92
+ */
93
+ private updateTaskForDowngrade;
94
+ /**
95
+ * 标记任务为失败
96
+ */
97
+ private markTaskFailed;
98
+ /**
99
+ * 缓存成功发送的消息
100
+ */
101
+ private cacheMessage;
102
+ /**
103
+ * 获取队列统计信息
104
+ */
105
+ getStats(): Promise<{
106
+ pending: number;
107
+ retry: number;
108
+ failed: number;
109
+ success: number;
110
+ }>;
111
+ /**
112
+ * 重试失败的任务
113
+ */
114
+ retryFailedTasks(taskId?: number): Promise<number>;
115
+ /**
116
+ * 清理旧的成功任务
117
+ */
118
+ cleanupSuccessTasks(olderThanHours?: number): Promise<number>;
119
+ }