@anyul/koishi-plugin-rss 5.2.3 → 5.2.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.
Files changed (70) hide show
  1. package/README.md +92 -37
  2. package/lib/commands/error-handler.js +13 -1
  3. package/lib/commands/index.d.ts +3 -0
  4. package/lib/commands/index.js +7 -1
  5. package/lib/commands/runtime.d.ts +17 -0
  6. package/lib/commands/runtime.js +27 -0
  7. package/lib/commands/subscription-create.d.ts +23 -0
  8. package/lib/commands/subscription-create.js +145 -0
  9. package/lib/commands/web-monitor.d.ts +15 -0
  10. package/lib/commands/web-monitor.js +222 -0
  11. package/lib/config.js +7 -1
  12. package/lib/constants.d.ts +1 -1
  13. package/lib/constants.js +46 -83
  14. package/lib/core/ai-cache.d.ts +27 -0
  15. package/lib/core/ai-cache.js +169 -0
  16. package/lib/core/ai-client.d.ts +12 -0
  17. package/lib/core/ai-client.js +65 -0
  18. package/lib/core/ai-selector.d.ts +2 -0
  19. package/lib/core/ai-selector.js +80 -0
  20. package/lib/core/ai-summary.d.ts +10 -0
  21. package/lib/core/ai-summary.js +73 -0
  22. package/lib/core/ai-utils.d.ts +10 -0
  23. package/lib/core/ai-utils.js +104 -0
  24. package/lib/core/ai.d.ts +3 -91
  25. package/lib/core/ai.js +13 -522
  26. package/lib/core/feeder-arg.d.ts +17 -0
  27. package/lib/core/feeder-arg.js +234 -0
  28. package/lib/core/feeder-runtime.d.ts +96 -0
  29. package/lib/core/feeder-runtime.js +233 -0
  30. package/lib/core/feeder.d.ts +3 -5
  31. package/lib/core/feeder.js +61 -358
  32. package/lib/core/item-processor-runtime.d.ts +46 -0
  33. package/lib/core/item-processor-runtime.js +215 -0
  34. package/lib/core/item-processor-template.d.ts +16 -0
  35. package/lib/core/item-processor-template.js +158 -0
  36. package/lib/core/item-processor.d.ts +1 -15
  37. package/lib/core/item-processor.js +44 -319
  38. package/lib/core/notification-queue-retry.d.ts +25 -0
  39. package/lib/core/notification-queue-retry.js +78 -0
  40. package/lib/core/notification-queue-sender.d.ts +20 -0
  41. package/lib/core/notification-queue-sender.js +118 -0
  42. package/lib/core/notification-queue-store.d.ts +19 -0
  43. package/lib/core/notification-queue-store.js +137 -0
  44. package/lib/core/notification-queue-types.d.ts +49 -0
  45. package/lib/core/notification-queue-types.js +2 -0
  46. package/lib/core/notification-queue.d.ts +11 -72
  47. package/lib/core/notification-queue.js +81 -258
  48. package/lib/core/search-format.d.ts +3 -0
  49. package/lib/core/search-format.js +36 -0
  50. package/lib/core/search-providers.d.ts +13 -0
  51. package/lib/core/search-providers.js +175 -0
  52. package/lib/core/search-rotation.d.ts +4 -0
  53. package/lib/core/search-rotation.js +55 -0
  54. package/lib/core/search-service.d.ts +3 -0
  55. package/lib/core/search-service.js +100 -0
  56. package/lib/core/search-types.d.ts +39 -0
  57. package/lib/core/search-types.js +2 -0
  58. package/lib/core/search.d.ts +4 -101
  59. package/lib/core/search.js +10 -508
  60. package/lib/index.js +27 -381
  61. package/lib/tsconfig.tsbuildinfo +1 -1
  62. package/lib/types.d.ts +27 -6
  63. package/lib/utils/legacy-config.d.ts +12 -0
  64. package/lib/utils/legacy-config.js +56 -0
  65. package/lib/utils/logger.js +50 -29
  66. package/lib/utils/proxy.d.ts +3 -0
  67. package/lib/utils/proxy.js +14 -0
  68. package/lib/utils/structured-logger.d.ts +7 -3
  69. package/lib/utils/structured-logger.js +26 -19
  70. package/package.json +1 -1
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatArg = formatArg;
4
+ exports.mixinArg = mixinArg;
5
+ const legacy_config_1 = require("../utils/legacy-config");
6
+ const logger_1 = require("../utils/logger");
7
+ const ARG_ENTRY_KEYS = [
8
+ 'forceLength',
9
+ 'reverse',
10
+ 'timeout',
11
+ 'interval',
12
+ 'merge',
13
+ 'maxRssItem',
14
+ 'firstLoad',
15
+ 'bodyWidth',
16
+ 'bodyPadding',
17
+ 'bodyFontSize',
18
+ 'split',
19
+ 'filter',
20
+ 'block',
21
+ 'proxyAgent',
22
+ ];
23
+ const BOOLEAN_ARG_KEYS = new Set(['firstLoad', 'reverse', 'merge']);
24
+ const NUMBER_ARG_KEYS = new Set([
25
+ 'forceLength',
26
+ 'timeout',
27
+ 'interval',
28
+ 'maxRssItem',
29
+ 'bodyWidth',
30
+ 'bodyPadding',
31
+ 'bodyFontSize',
32
+ 'split',
33
+ ]);
34
+ const ARRAY_ARG_KEYS = new Set(['filter', 'block']);
35
+ const FALSE_CONTENT = new Set(['false', 'null', 'none', '']);
36
+ function parseArrayArg(value) {
37
+ return value
38
+ .split('/')
39
+ .map(item => item.trim())
40
+ .filter(Boolean);
41
+ }
42
+ function extractKnownArgEntries(arg) {
43
+ if (!arg)
44
+ return {};
45
+ const pattern = new RegExp(`(^|,)\\s*(${ARG_ENTRY_KEYS.join('|')})\\s*:`, 'g');
46
+ const matches = [...arg.matchAll(pattern)];
47
+ const result = {};
48
+ for (let index = 0; index < matches.length; index++) {
49
+ const currentMatch = matches[index];
50
+ const nextMatch = matches[index + 1];
51
+ const key = currentMatch[2];
52
+ const valueStart = (currentMatch.index ?? 0) + currentMatch[0].length;
53
+ const valueEnd = nextMatch?.index ?? arg.length;
54
+ result[key] = arg.slice(valueStart, valueEnd).replace(/,\s*$/, '').trim();
55
+ }
56
+ return result;
57
+ }
58
+ function parseBooleanArg(value) {
59
+ return !FALSE_CONTENT.has(String(value ?? '').trim().toLowerCase());
60
+ }
61
+ function parseNumberArg(value) {
62
+ const parsed = Number(value);
63
+ return Number.isFinite(parsed) ? parsed : undefined;
64
+ }
65
+ function normalizeScalarArgValue(value) {
66
+ if (typeof value !== 'string') {
67
+ return value;
68
+ }
69
+ return value.split(',')[0].trim();
70
+ }
71
+ function parseProxyAgentArg(value, auth) {
72
+ if (typeof value === 'object' && value !== null) {
73
+ return value;
74
+ }
75
+ const normalizedValue = String(normalizeScalarArgValue(value) ?? '').trim();
76
+ if (FALSE_CONTENT.has(normalizedValue.toLowerCase())) {
77
+ return { enabled: false };
78
+ }
79
+ const proxyUrl = normalizedValue.includes('://') ? normalizedValue : `http://${normalizedValue}`;
80
+ try {
81
+ const parsedUrl = new URL(proxyUrl);
82
+ const protocol = parsedUrl.protocol.replace(':', '') || 'http';
83
+ const host = parsedUrl.hostname;
84
+ const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 7890;
85
+ if (!host)
86
+ return undefined;
87
+ const proxyAgent = {
88
+ enabled: true,
89
+ protocol,
90
+ host,
91
+ port,
92
+ };
93
+ if (auth) {
94
+ const [username = '', password = ''] = auth.split('/');
95
+ if (username) {
96
+ proxyAgent.auth = { enabled: true, username, password };
97
+ }
98
+ }
99
+ return proxyAgent;
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ }
105
+ function mergeProxyAgent(argProxy, configProxy, config) {
106
+ (0, logger_1.debug)(config, `合并代理配置 - argProxy: ${JSON.stringify(argProxy)}, configProxy.enabled: ${configProxy?.enabled}`, 'proxy merge debug', 'details');
107
+ if (argProxy?.enabled === false) {
108
+ (0, logger_1.debug)(config, '订阅明确禁用代理', 'proxy merge', 'details');
109
+ return { enabled: false };
110
+ }
111
+ if (argProxy?.enabled === true && argProxy?.host) {
112
+ (0, logger_1.debug)(config, '使用订阅的代理配置', 'proxy merge', 'details');
113
+ return argProxy;
114
+ }
115
+ const shouldUseConfigProxy = !argProxy || Object.keys(argProxy || {}).length === 0 || argProxy?.enabled === undefined || argProxy?.enabled === null;
116
+ if (shouldUseConfigProxy) {
117
+ if (configProxy?.enabled) {
118
+ const result = {
119
+ enabled: true,
120
+ protocol: configProxy.protocol,
121
+ host: configProxy.host,
122
+ port: configProxy.port,
123
+ auth: configProxy.auth?.enabled ? configProxy.auth : undefined,
124
+ };
125
+ (0, logger_1.debug)(config, `使用全局代理: ${result.protocol}://${result.host}:${result.port}`, 'proxy merge', 'info');
126
+ return result;
127
+ }
128
+ (0, logger_1.debug)(config, '全局代理未启用', 'proxy merge', 'details');
129
+ }
130
+ if (argProxy?.enabled === true && !argProxy?.host) {
131
+ const result = {
132
+ ...configProxy,
133
+ ...argProxy,
134
+ auth: configProxy?.auth?.enabled ? configProxy.auth : undefined,
135
+ };
136
+ (0, logger_1.debug)(config, '订阅代理配置不完整,补充全局配置', 'proxy merge', 'details');
137
+ return result;
138
+ }
139
+ (0, logger_1.debug)(config, '代理未配置,使用默认(禁用)', 'proxy merge', 'details');
140
+ return { enabled: false };
141
+ }
142
+ function mergeProxyAgentWithLog(argProxy, configProxy, config) {
143
+ const result = mergeProxyAgent(argProxy, configProxy, config);
144
+ (0, logger_1.debug)(config, `[DEBUG_PROXY] mergeProxyAgent input: arg=${JSON.stringify(argProxy)} conf=${JSON.stringify(configProxy)} output=${JSON.stringify(result)}`, 'proxy merge', 'details');
145
+ return result;
146
+ }
147
+ /**
148
+ * 将命令选项解析为订阅参数对象。
149
+ *
150
+ * @param options - 命令选项
151
+ * @param config - 插件配置
152
+ * @returns 解析后的订阅参数
153
+ */
154
+ function formatArg(options, config) {
155
+ const { arg, template, auth } = options || {};
156
+ const rawEntries = typeof arg === 'string'
157
+ ? extractKnownArgEntries(arg)
158
+ : (arg || {});
159
+ const json = {};
160
+ for (const [rawKey, rawValue] of Object.entries(rawEntries)) {
161
+ const key = rawKey;
162
+ if (!ARG_ENTRY_KEYS.includes(key))
163
+ continue;
164
+ if (key === 'proxyAgent') {
165
+ const proxyAgent = parseProxyAgentArg(rawValue, auth);
166
+ if (proxyAgent) {
167
+ json.proxyAgent = proxyAgent;
168
+ }
169
+ continue;
170
+ }
171
+ if (ARRAY_ARG_KEYS.has(key)) {
172
+ if (Array.isArray(rawValue)) {
173
+ json[key] = rawValue.filter(Boolean);
174
+ }
175
+ else {
176
+ const parsedArray = parseArrayArg(String(rawValue ?? ''));
177
+ if (parsedArray.length) {
178
+ json[key] = parsedArray;
179
+ }
180
+ }
181
+ continue;
182
+ }
183
+ if (BOOLEAN_ARG_KEYS.has(key)) {
184
+ json[key] = parseBooleanArg(normalizeScalarArgValue(rawValue));
185
+ continue;
186
+ }
187
+ if (NUMBER_ARG_KEYS.has(key)) {
188
+ const parsedNumber = parseNumberArg(normalizeScalarArgValue(rawValue));
189
+ if (parsedNumber !== undefined) {
190
+ json[key] = parsedNumber;
191
+ }
192
+ continue;
193
+ }
194
+ if (rawValue !== undefined && rawValue !== null && String(rawValue).trim() !== '') {
195
+ json[key] = String(rawValue).trim();
196
+ }
197
+ }
198
+ if (template && config.template) {
199
+ json.template = template;
200
+ }
201
+ if (typeof json.interval === 'number')
202
+ json.interval *= 1000;
203
+ return json;
204
+ }
205
+ /**
206
+ * 合并全局配置与订阅级参数。
207
+ *
208
+ * @param arg - 订阅级参数
209
+ * @param config - 插件配置
210
+ * @returns 合并后的运行时参数
211
+ */
212
+ function mixinArg(arg, config) {
213
+ const normalizedArg = (0, legacy_config_1.normalizeSubscriptionArg)(arg);
214
+ const mergedProxy = mergeProxyAgentWithLog(normalizedArg?.proxyAgent, config.net?.proxyAgent, config);
215
+ if (mergedProxy?.enabled) {
216
+ (0, logger_1.debug)(config, `使用代理: ${mergedProxy.protocol}://${mergedProxy.host}:${mergedProxy.port}`, 'proxy merge', 'details');
217
+ }
218
+ else {
219
+ (0, logger_1.debug)(config, '代理未启用', 'proxy merge', 'details');
220
+ }
221
+ const baseConfig = (0, legacy_config_1.normalizeBasicConfig)({
222
+ ...config.basic,
223
+ });
224
+ const result = (0, legacy_config_1.normalizeSubscriptionArg)({
225
+ ...baseConfig,
226
+ ...normalizedArg,
227
+ filter: [...(config.msg?.keywordFilter || []), ...(normalizedArg?.filter || [])],
228
+ block: [...(config.msg?.keywordBlock || []), ...(normalizedArg?.block || [])],
229
+ template: normalizedArg.template ?? baseConfig.defaultTemplate,
230
+ proxyAgent: mergedProxy,
231
+ });
232
+ (0, logger_1.debug)(config, `[DEBUG_PROXY] mixinArg return: ${JSON.stringify(result.proxyAgent)}`, 'mixin', 'details');
233
+ return result;
234
+ }
@@ -0,0 +1,96 @@
1
+ import { Context } from 'koishi';
2
+ import { Config, rssArg } from '../types';
3
+ import { createDebugWithContext } from '../utils/logger';
4
+ import { RssItemProcessor } from './item-processor';
5
+ type FeedDebugFn = ReturnType<typeof createDebugWithContext>;
6
+ /**
7
+ * 构建订阅抓取日志上下文。
8
+ *
9
+ * @param rssItem - 订阅记录
10
+ * @returns 日志上下文
11
+ */
12
+ export declare function buildFeedLogContext(rssItem: any): Record<string, any>;
13
+ /**
14
+ * 创建带订阅上下文的调试函数。
15
+ *
16
+ * @param config - 插件配置
17
+ * @param rssItem - 订阅记录
18
+ * @returns 调试函数
19
+ */
20
+ export declare function createFeedDebug(config: Config, rssItem: any): FeedDebugFn;
21
+ /**
22
+ * 根据序号、URL 或标题查找订阅项。
23
+ *
24
+ * @param rssList - 订阅列表
25
+ * @param keyword - 查找关键字
26
+ * @returns 匹配到的订阅项
27
+ */
28
+ export declare function findRssItem(rssList: any[], keyword: number | string): any;
29
+ /**
30
+ * 提取用于去重的内容字段。
31
+ *
32
+ * @param item - RSS 条目
33
+ * @param _config - 插件配置(保留兼容签名)
34
+ * @returns 去重内容对象
35
+ */
36
+ export declare function getLastContent(item: any, _config: Config): any;
37
+ /**
38
+ * 抓取订阅的所有 RSS 条目。
39
+ *
40
+ * @param ctx - Koishi 上下文
41
+ * @param config - 插件配置
42
+ * @param $http - HTTP 函数
43
+ * @param rssItem - 订阅记录
44
+ * @param arg - 运行时参数
45
+ * @param feedDebug - 调试函数
46
+ * @returns 抓取到的条目列表
47
+ */
48
+ export declare function fetchRssItems(ctx: Context, config: Config, $http: any, rssItem: any, arg: rssArg, feedDebug: FeedDebugFn): Promise<any[]>;
49
+ /**
50
+ * 按过滤词过滤抓取结果。
51
+ *
52
+ * @param items - 原始条目列表
53
+ * @param arg - 运行时参数
54
+ * @param feedDebug - 调试函数
55
+ * @returns 过滤后的条目列表
56
+ */
57
+ export declare function filterItems(items: any[], arg: rssArg, feedDebug: FeedDebugFn): any[];
58
+ /**
59
+ * 检查条目是否为新增或更新内容。
60
+ *
61
+ * @param config - 插件配置
62
+ * @param rssItem - 订阅记录
63
+ * @param items - 过滤后的条目列表
64
+ * @param arg - 运行时参数
65
+ * @param feedDebug - 调试函数
66
+ * @returns 新条目、最新发布时间与当前去重内容
67
+ */
68
+ export declare function checkForUpdates(config: Config, rssItem: any, items: any[], arg: rssArg, feedDebug: FeedDebugFn): {
69
+ newItems: any[];
70
+ latestPubDate: Date;
71
+ currentContent: any[];
72
+ };
73
+ /**
74
+ * 将 RSS 条目解析为待发送消息。
75
+ *
76
+ * @param processor - 条目处理器
77
+ * @param items - 待发送条目
78
+ * @param rssItem - 订阅记录
79
+ * @param arg - 运行时参数
80
+ * @returns 消息列表与发送顺序条目列表
81
+ */
82
+ export declare function generateMessages(processor: RssItemProcessor, items: any[], rssItem: any, arg: rssArg): Promise<{
83
+ messageList: string[];
84
+ itemsToSend: any[];
85
+ }>;
86
+ /**
87
+ * 构建最终入队消息内容。
88
+ *
89
+ * @param config - 插件配置
90
+ * @param messageList - 消息片段列表
91
+ * @param rssItem - 订阅记录
92
+ * @param arg - 运行时参数
93
+ * @returns 最终发送消息
94
+ */
95
+ export declare function buildFinalMessage(config: Config, messageList: string[], rssItem: any, arg: rssArg): string;
96
+ export {};
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildFeedLogContext = buildFeedLogContext;
4
+ exports.createFeedDebug = createFeedDebug;
5
+ exports.findRssItem = findRssItem;
6
+ exports.getLastContent = getLastContent;
7
+ exports.fetchRssItems = fetchRssItems;
8
+ exports.filterItems = filterItems;
9
+ exports.checkForUpdates = checkForUpdates;
10
+ exports.generateMessages = generateMessages;
11
+ exports.buildFinalMessage = buildFinalMessage;
12
+ const koishi_1 = require("koishi");
13
+ const constants_1 = require("../constants");
14
+ const common_1 = require("../utils/common");
15
+ const error_handler_1 = require("../utils/error-handler");
16
+ const error_tracker_1 = require("../utils/error-tracker");
17
+ const legacy_config_1 = require("../utils/legacy-config");
18
+ const logger_1 = require("../utils/logger");
19
+ const parser_1 = require("./parser");
20
+ /**
21
+ * 构建订阅抓取日志上下文。
22
+ *
23
+ * @param rssItem - 订阅记录
24
+ * @returns 日志上下文
25
+ */
26
+ function buildFeedLogContext(rssItem) {
27
+ return {
28
+ subscribeId: String(rssItem.id),
29
+ rssId: rssItem.rssId || rssItem.title,
30
+ rssTitle: rssItem.title,
31
+ url: rssItem.url,
32
+ guildId: rssItem.guildId,
33
+ platform: rssItem.platform,
34
+ };
35
+ }
36
+ /**
37
+ * 创建带订阅上下文的调试函数。
38
+ *
39
+ * @param config - 插件配置
40
+ * @param rssItem - 订阅记录
41
+ * @returns 调试函数
42
+ */
43
+ function createFeedDebug(config, rssItem) {
44
+ return (0, logger_1.createDebugWithContext)(config, buildFeedLogContext(rssItem));
45
+ }
46
+ /**
47
+ * 根据序号、URL 或标题查找订阅项。
48
+ *
49
+ * @param rssList - 订阅列表
50
+ * @param keyword - 查找关键字
51
+ * @returns 匹配到的订阅项
52
+ */
53
+ function findRssItem(rssList, keyword) {
54
+ if (typeof keyword === 'number' || /^\d+$/.test(String(keyword))) {
55
+ const listIndex = parseInt(String(keyword)) - 1;
56
+ if (listIndex >= 0 && listIndex < rssList.length) {
57
+ return rssList[listIndex];
58
+ }
59
+ }
60
+ const index = ((rssList.findIndex(i => i.rssId === +keyword) + 1) ||
61
+ (rssList.findIndex(i => i.url == keyword) + 1) ||
62
+ (rssList.findIndex(i => i.url.indexOf(keyword) + 1) + 1) ||
63
+ (rssList.findIndex(i => i.title.indexOf(keyword) + 1) + 1)) - 1;
64
+ if (index < 0 || index >= rssList.length) {
65
+ return undefined;
66
+ }
67
+ return rssList[index];
68
+ }
69
+ /**
70
+ * 提取用于去重的内容字段。
71
+ *
72
+ * @param item - RSS 条目
73
+ * @param _config - 插件配置(保留兼容签名)
74
+ * @returns 去重内容对象
75
+ */
76
+ function getLastContent(item, _config) {
77
+ const keys = ['title', 'description', 'link', 'guid'];
78
+ const obj = Object.assign({}, ...keys.map(key => (0, koishi_1.clone)(item?.[key]) ? { [key]: item[key] } : {}));
79
+ return { ...obj, description: String(obj?.description).replaceAll(/\s/g, '') };
80
+ }
81
+ /**
82
+ * 抓取订阅的所有 RSS 条目。
83
+ *
84
+ * @param ctx - Koishi 上下文
85
+ * @param config - 插件配置
86
+ * @param $http - HTTP 函数
87
+ * @param rssItem - 订阅记录
88
+ * @param arg - 运行时参数
89
+ * @param feedDebug - 调试函数
90
+ * @returns 抓取到的条目列表
91
+ */
92
+ async function fetchRssItems(ctx, config, $http, rssItem, arg, feedDebug) {
93
+ const rssHubUrl = config.msg?.rssHubUrl || 'https://hub.slarker.me';
94
+ try {
95
+ const urls = rssItem.url.split('|').map((url) => (0, common_1.parseQuickUrl)(url, rssHubUrl, constants_1.quickList));
96
+ const results = await Promise.all(urls.map(async (url) => await (0, parser_1.getRssData)(ctx, config, $http, url, arg)));
97
+ return results.flat(1);
98
+ }
99
+ catch (error) {
100
+ const normalizedError = (0, error_handler_1.normalizeError)(error);
101
+ feedDebug(`Fetch failed for ${rssItem.title}: ${normalizedError.message}`, 'feeder', 'error', {
102
+ stage: 'fetch',
103
+ });
104
+ (0, error_tracker_1.trackError)(normalizedError, {
105
+ ...buildFeedLogContext(rssItem),
106
+ stage: 'fetch',
107
+ });
108
+ return [];
109
+ }
110
+ }
111
+ /**
112
+ * 按过滤词过滤抓取结果。
113
+ *
114
+ * @param items - 原始条目列表
115
+ * @param arg - 运行时参数
116
+ * @param feedDebug - 调试函数
117
+ * @returns 过滤后的条目列表
118
+ */
119
+ function filterItems(items, arg, feedDebug) {
120
+ return items.filter(item => {
121
+ const matchKeyword = arg.filter?.find((keyword) => new RegExp(keyword, 'im').test(item.title) || new RegExp(keyword, 'im').test(item.description));
122
+ if (matchKeyword) {
123
+ feedDebug(`filter:${matchKeyword}`, 'feeder', 'info', { matchedKeyword: matchKeyword });
124
+ feedDebug(item, 'filter rss item', 'info', { matchedKeyword: matchKeyword });
125
+ }
126
+ return !matchKeyword;
127
+ });
128
+ }
129
+ /**
130
+ * 检查条目是否为新增或更新内容。
131
+ *
132
+ * @param config - 插件配置
133
+ * @param rssItem - 订阅记录
134
+ * @param items - 过滤后的条目列表
135
+ * @param arg - 运行时参数
136
+ * @param feedDebug - 调试函数
137
+ * @returns 新条目、最新发布时间与当前去重内容
138
+ */
139
+ function checkForUpdates(config, rssItem, items, arg, feedDebug) {
140
+ const resendUpdatedContent = (0, legacy_config_1.getResendUpdatedContent)(config);
141
+ let itemArray = items
142
+ .sort((a, b) => (0, common_1.parsePubDate)(config, b.pubDate).getTime() - (0, common_1.parsePubDate)(config, a.pubDate).getTime());
143
+ if (itemArray.length === 0) {
144
+ return { newItems: [], latestPubDate: new Date(), currentContent: [] };
145
+ }
146
+ const latestItem = itemArray[0];
147
+ const lastPubDate = (0, common_1.parsePubDate)(config, latestItem.pubDate);
148
+ feedDebug(`${rssItem.title}: Latest item date=${lastPubDate.toISOString()}, DB date=${rssItem.lastPubDate ? new Date(rssItem.lastPubDate).toISOString() : 'none'}`, 'feeder', 'details');
149
+ const currentContent = resendUpdatedContent === 'all'
150
+ ? itemArray.map((item) => getLastContent(item, config))
151
+ : [getLastContent(latestItem, config)];
152
+ if (arg.reverse) {
153
+ itemArray = itemArray.reverse();
154
+ }
155
+ let rssItemArray = [];
156
+ if (rssItem.arg.forceLength) {
157
+ rssItemArray = itemArray.slice(0, rssItem.arg.forceLength);
158
+ feedDebug(`${rssItem.title}: Force length mode, taking ${rssItemArray.length} items`, 'feeder', 'details');
159
+ }
160
+ else {
161
+ feedDebug(`${rssItem.title}: Checking ${itemArray.length} items for updates`, 'feeder', 'details');
162
+ rssItemArray = itemArray.filter((item, index) => {
163
+ const currentItemTime = (0, common_1.parsePubDate)(config, item.pubDate).getTime();
164
+ const lastTime = rssItem.lastPubDate ? (0, common_1.parsePubDate)(config, rssItem.lastPubDate).getTime() : 0;
165
+ feedDebug(`[${index}] ${item.title?.substring(0, 30)}: time=${new Date(currentItemTime).toISOString()} > last=${new Date(lastTime).toISOString()} ? ${currentItemTime > lastTime}`, 'feeder', 'details');
166
+ if (currentItemTime > lastTime) {
167
+ feedDebug(`[${index}] ✓ Item is new (time check)`, 'feeder', 'details');
168
+ return true;
169
+ }
170
+ if (resendUpdatedContent !== 'disable') {
171
+ const newItemContent = getLastContent(item, config);
172
+ const oldItemMatch = rssItem.lastContent?.itemArray?.find((old) => (newItemContent.guid && old.guid === newItemContent.guid) ||
173
+ (old.link === newItemContent.link && old.title === newItemContent.title));
174
+ if (oldItemMatch) {
175
+ const descriptionChanged = JSON.stringify(oldItemMatch.description) !== JSON.stringify(newItemContent.description);
176
+ if (descriptionChanged) {
177
+ feedDebug(`[${index}] ✓ Item is updated (content changed)`, 'feeder', 'details');
178
+ }
179
+ else {
180
+ feedDebug(`[${index}] ✗ Item filtered (already sent)`, 'feeder', 'details');
181
+ }
182
+ return descriptionChanged;
183
+ }
184
+ feedDebug(`[${index}] ✗ Item filtered (no match in lastContent)`, 'feeder', 'details');
185
+ }
186
+ feedDebug(`[${index}] ✗ Item filtered (failed all checks)`, 'feeder', 'details');
187
+ return false;
188
+ });
189
+ if (arg.maxRssItem) {
190
+ rssItemArray = rssItemArray.slice(0, arg.maxRssItem);
191
+ }
192
+ }
193
+ return { newItems: rssItemArray, latestPubDate: lastPubDate, currentContent };
194
+ }
195
+ /**
196
+ * 将 RSS 条目解析为待发送消息。
197
+ *
198
+ * @param processor - 条目处理器
199
+ * @param items - 待发送条目
200
+ * @param rssItem - 订阅记录
201
+ * @param arg - 运行时参数
202
+ * @returns 消息列表与发送顺序条目列表
203
+ */
204
+ async function generateMessages(processor, items, rssItem, arg) {
205
+ const itemsToSend = [...items].reverse();
206
+ const messageList = (await Promise.all(itemsToSend.map(async (item) => await processor.parseRssItem(item, { ...rssItem, ...arg }, rssItem.author)))).filter(message => message);
207
+ return { messageList, itemsToSend };
208
+ }
209
+ /**
210
+ * 构建最终入队消息内容。
211
+ *
212
+ * @param config - 插件配置
213
+ * @param messageList - 消息片段列表
214
+ * @param rssItem - 订阅记录
215
+ * @param arg - 运行时参数
216
+ * @returns 最终发送消息
217
+ */
218
+ function buildFinalMessage(config, messageList, rssItem, arg) {
219
+ let message = '';
220
+ const shouldMerge = arg.merge === true || config.basic?.merge === '一直合并' || (config.basic?.merge === '有多条更新时合并' && messageList.length > 1);
221
+ const hasVideo = (0, legacy_config_1.shouldMergeVideo)(config) && messageList.some(msg => /<video/.test(msg));
222
+ if (shouldMerge || hasVideo) {
223
+ message = `<message forward><author id="${rssItem.author}"/>${messageList.map(msg => `<message>${msg}</message>`).join('')}</message>`;
224
+ }
225
+ else {
226
+ message = messageList.join('');
227
+ }
228
+ if (rssItem.followers && rssItem.followers.length > 0) {
229
+ const mentions = rssItem.followers.map((id) => `<at ${id === 'all' ? 'type="all"' : `id="${id}"`}/>`).join(' ');
230
+ message += `<message>${mentions}</message>`;
231
+ }
232
+ return message;
233
+ }
@@ -1,5 +1,5 @@
1
1
  import { Context } from 'koishi';
2
- import { Config, rssArg } from '../types';
2
+ import { Config } from '../types';
3
3
  import { RssItemProcessor } from './item-processor';
4
4
  import { NotificationQueueManager } from './notification-queue';
5
5
  export interface FeederDependencies {
@@ -8,10 +8,8 @@ export interface FeederDependencies {
8
8
  $http: any;
9
9
  queueManager: NotificationQueueManager;
10
10
  }
11
- export declare function findRssItem(rssList: any[], keyword: number | string): any;
12
- export declare function getLastContent(item: any, config: Config): any;
13
- export declare function formatArg(options: any, config: Config): rssArg;
14
- export declare function mixinArg(arg: any, config: Config): rssArg;
11
+ export { formatArg, mixinArg } from './feeder-arg';
12
+ export { findRssItem, getLastContent } from './feeder-runtime';
15
13
  /**
16
14
  * 生产者:抓取 RSS,发现新消息,存入队列
17
15
  */