@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
@@ -34,17 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.RssItemProcessor = void 0;
37
- const koishi_1 = require("koishi");
38
37
  const cheerio = __importStar(require("cheerio"));
39
38
  const logger_1 = require("../utils/logger");
40
- const common_1 = require("../utils/common");
41
- const media_1 = require("../utils/media");
42
- const renderer_1 = require("./renderer");
43
- const template_1 = require("../utils/template");
44
- const ai_1 = require("./ai");
45
- const marked_1 = require("marked");
46
39
  const sanitizer_1 = require("../utils/sanitizer");
47
- const security_1 = require("../utils/security");
40
+ const ai_1 = require("./ai");
41
+ const item_processor_template_1 = require("./item-processor-template");
42
+ const item_processor_runtime_1 = require("./item-processor-runtime");
48
43
  class RssItemProcessor {
49
44
  ctx;
50
45
  config;
@@ -54,345 +49,75 @@ class RssItemProcessor {
54
49
  this.config = config;
55
50
  this.$http = $http;
56
51
  }
52
+ getRuntimeDeps() {
53
+ return {
54
+ ctx: this.ctx,
55
+ config: this.config,
56
+ $http: this.$http,
57
+ };
58
+ }
57
59
  async parseRssItem(item, arg, authorId) {
60
+ void authorId;
58
61
  (0, logger_1.debug)(this.config, arg, 'rss arg', 'details');
59
62
  let template = arg.template;
60
- let msg = "";
61
- let html;
62
- let videoList = [];
63
- item.description = item.description?.join?.('') || item.description;
64
- // HTML 安全清理
63
+ item.title = (0, item_processor_runtime_1.normalizeText)(item?.title);
64
+ item.description = (0, item_processor_runtime_1.normalizeText)(item?.description);
65
65
  const sanitizer = (0, sanitizer_1.createSanitizer)(this.config);
66
66
  if (sanitizer.isEnabled() && item.description) {
67
67
  item.description = sanitizer.sanitize(item.description);
68
68
  }
69
- // --- AI 逻辑 START ---
70
- let aiSummary = "";
71
- let formattedAiSummary = "";
72
- const hasCustomAiTemplate = this.config.template?.custom?.includes('{{aiSummary}}') ||
73
- this.config.template?.content?.includes('{{aiSummary}}');
74
- if (this.config.ai && this.config.ai.enabled) {
69
+ let aiSummary = '';
70
+ let formattedAiSummary = '';
71
+ const hasCustomAiTemplate = this.config.template?.custom?.includes('{{aiSummary}}')
72
+ || this.config.template?.content?.includes('{{aiSummary}}');
73
+ if (this.config.ai?.enabled) {
75
74
  const rawSummary = await (0, ai_1.getAiSummary)(this.config, item.title, item.description);
76
75
  if (rawSummary) {
77
- const prefix = "🤖 AI摘要:\n";
78
- const sep = this.config.ai?.separator || '----------------';
79
- // 带格式的摘要文本
80
- formattedAiSummary = `${prefix}${rawSummary}`;
81
- // 注入模板变量的纯文本
82
76
  aiSummary = rawSummary;
83
- // aiSummary 添加到 item 对象中,供模板使用
77
+ formattedAiSummary = `🤖 AI摘要:\n${rawSummary}`;
84
78
  item.aiSummary = aiSummary;
85
79
  }
86
80
  }
87
- // --- AI 逻辑 END ---
88
- //block
89
81
  arg.block?.forEach((blockWord) => {
90
- item.description = item.description.replace(new RegExp(blockWord, 'gim'), i => Array(i.length).fill(this.config.msg?.blockString || '*').join(""));
91
- item.title = item.title.replace(new RegExp(blockWord, 'gim'), i => Array(i.length).fill(this.config.msg?.blockString || '*').join(""));
82
+ item.description = (0, item_processor_runtime_1.normalizeText)(item.description).replace(new RegExp(blockWord, 'gim'), matched => Array(matched.length).fill(this.config.msg?.blockString || '*').join(''));
83
+ item.title = (0, item_processor_runtime_1.normalizeText)(item.title).replace(new RegExp(blockWord, 'gim'), matched => Array(matched.length).fill(this.config.msg?.blockString || '*').join(''));
92
84
  });
93
- if (this.config.basic?.videoMode === 'filter') {
94
- html = cheerio.load(item.description);
95
- if (html('video').length > 0)
96
- return '';
85
+ const html = cheerio.load(item.description);
86
+ if (this.config.basic?.videoMode === 'filter' && html('video').length > 0) {
87
+ return '';
97
88
  }
98
- html = cheerio.load(item.description);
99
- if (template == 'auto') {
100
- let stringLength = html.text().length;
101
- template = stringLength < 300 ? 'content' : 'custom';
89
+ if (template === 'auto') {
90
+ template = html.text().length < 300 ? 'content' : 'custom';
102
91
  }
103
- // 在确定最终模板后才记录日志
104
92
  if (template) {
105
93
  (0, logger_1.debug)(this.config, `使用模板: ${template}`, 'template', 'info');
106
94
  }
107
- msg = await this.processTemplate(template, item, arg, html, videoList, aiSummary);
108
- // 如果是图片渲染模板,AI摘要已经被注入到HTML中,不需要再自动拼接
109
- const isImageRenderTemplate = template === 'custom' || template === 'default' || template === 'only description';
110
- if (isImageRenderTemplate && (this.config.basic?.imageMode === 'base64' || this.config.basic?.imageMode === 'File' || this.config.basic?.imageMode === 'assets')) {
95
+ let msg = await (0, item_processor_template_1.processItemTemplate)({
96
+ deps: this.getRuntimeDeps(),
97
+ template,
98
+ item,
99
+ arg,
100
+ html,
101
+ aiSummary,
102
+ });
103
+ const imageMode = this.config.basic?.imageMode;
104
+ const isImageRenderTemplate = template === 'custom'
105
+ || template === 'default'
106
+ || template === 'only description';
107
+ if (isImageRenderTemplate && ['base64', 'File', 'assets'].includes(imageMode || '')) {
111
108
  formattedAiSummary = '';
112
109
  }
113
110
  if (this.config.msg?.censor) {
114
111
  msg = `<censor>${msg}</censor>`;
115
112
  }
116
- // --- AI 自动拼接逻辑 START ---
117
- // 如果生成了摘要,且用户使用的模板里没有显式包含 {{aiSummary}},则自动拼接
118
113
  if (formattedAiSummary && !hasCustomAiTemplate && this.config.ai) {
119
- const sep = this.config.ai?.separator || '----------------';
120
- if (this.config.ai?.placement === 'bottom') {
121
- // 底部:正文 + 分割线 + 摘要
122
- msg = msg + `\n${sep}\n` + formattedAiSummary;
123
- }
124
- else {
125
- // 顶部:摘要 + 分割线 + 正文
126
- msg = formattedAiSummary + `\n${sep}\n` + msg;
127
- }
128
- }
129
- // --- AI 自动拼接逻辑 END ---
130
- (0, logger_1.debug)(this.config, msg, "parse:msg", 'info');
131
- return msg;
132
- }
133
- async processTemplate(template, item, arg, html, videoList, aiSummary) {
134
- let msg = "";
135
- const parseContent = (templateStr, itemObj) => (0, common_1.parseTemplateContent)(templateStr, { ...itemObj, aiSummary });
136
- switch (template) {
137
- case "custom":
138
- msg = await this.processCustomTemplate(item, arg, html, parseContent);
139
- await this.processVideos(html, arg, videoList);
140
- msg += this.formatVideoList(videoList);
141
- break;
142
- case "content":
143
- msg = await this.processContentTemplate(item, arg, html, parseContent);
144
- await this.processVideos(html, arg, videoList);
145
- msg += this.formatVideoList(videoList);
146
- msg += videoList.filter(([src, poster]) => poster && !src.startsWith('__VIDEO_LINK__')).map(([src, poster]) => (0, koishi_1.h)('img', { src: poster })).join("");
147
- break;
148
- case "only text":
149
- msg = html.text();
150
- break;
151
- case "only media":
152
- msg = await this.processOnlyMediaTemplate(item, arg, html);
153
- await this.processVideos(html, arg, videoList);
154
- msg += this.formatVideoList(videoList);
155
- break;
156
- case "only image":
157
- msg = await this.processOnlyImageTemplate(item, arg, html);
158
- break;
159
- case "only video":
160
- await this.processVideos(html, arg, videoList);
161
- msg = this.formatVideoList(videoList);
162
- break;
163
- case "proto":
164
- msg = item.description;
165
- break;
166
- case "default":
167
- msg = await this.processDefaultTemplate(item, arg, html, parseContent);
168
- await this.processVideos(html, arg, videoList);
169
- msg += this.formatVideoList(videoList);
170
- break;
171
- case "only description":
172
- msg = await this.processOnlyDescriptionTemplate(item, arg, html, parseContent);
173
- await this.processVideos(html, arg, videoList);
174
- msg += this.formatVideoList(videoList);
175
- break;
176
- case "link":
177
- msg = await this.processLinkTemplate(item, arg);
178
- break;
179
- default:
180
- msg = item.description;
181
- }
182
- return msg;
183
- }
184
- async processCustomTemplate(item, arg, html, parseContent) {
185
- item.description = parseContent(this.config.template?.custom || '', { ...item, arg });
186
- (0, logger_1.debug)(this.config, item.description, 'description');
187
- // 如果有AI摘要,在图片渲染前将其注入到HTML中
188
- const hasAiSummary = item.aiSummary && item.aiSummary.trim();
189
- if (hasAiSummary && (this.config.basic?.imageMode === 'base64' || this.config.basic?.imageMode === 'File' || this.config.basic?.imageMode === 'assets')) {
190
- // 将markdown转换为HTML
191
- const aiSummaryHtml = await (0, marked_1.marked)(item.aiSummary);
192
- const aiSummarySection = `
193
- <div class="ai-summary-section mb-6">
194
- <div class="flex items-start gap-3 mb-3">
195
- <div class="mt-0.5 w-6 h-6 rounded-md bg-primary/10 flex flex-shrink-0 items-center justify-center">
196
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 text-primary">
197
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
198
- </svg>
199
- </div>
200
- <h3 class="text-sm font-bold text-slate-700">AI 摘要</h3>
201
- </div>
202
- <div class="pl-9 prose prose-slate prose-sm max-w-none">
203
- ${aiSummaryHtml}
204
- </div>
205
- </div>
206
- <div class="border-t border-slate-100 my-6"></div>
207
- `;
208
- item.description = aiSummarySection + item.description;
209
- }
210
- html = cheerio.load(item.description);
211
- if (arg?.proxyAgent?.enabled) {
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());
213
- }
214
- html('img').attr('style', 'object-fit:scale-down;max-width:100%;');
215
- let msg = await this.renderImage(html.html(), arg);
216
- return parseContent(this.config.template?.customRemark || '', { ...item, arg, description: msg });
217
- }
218
- async processContentTemplate(item, arg, html, parseContent) {
219
- let imgList = [];
220
- html('img').map((key, i) => imgList.push(i.attribs.src));
221
- imgList = [...new Set(imgList)];
222
- let imgBufferList = Object.assign({}, ...(await Promise.all(imgList.map(async (src) => ({ [src]: await (0, media_1.getImageUrl)(this.ctx, this.config, this.$http, src, arg) })))));
223
- html('img').replaceWith((key, Dom) => `<p>$img{{${imgList[key]}}}</p>`);
224
- let msg = html.text();
225
- item.description = msg.replace(/\$img\{\{(.*?)\}\}/g, (match) => {
226
- let src = match.match(/\$img\{\{(.*?)\}\}/)[1];
227
- let finalUrl = imgBufferList[src];
228
- return finalUrl ? `<img src="${finalUrl}"/>` : '';
229
- });
230
- return parseContent(this.config.template?.content || '', { ...item, arg });
231
- }
232
- async processOnlyMediaTemplate(item, arg, html) {
233
- let imgList = [];
234
- html('img').map((key, i) => imgList.push(i.attribs.src));
235
- imgList = await Promise.all([...new Set(imgList)].map(async (src) => await (0, media_1.getImageUrl)(this.ctx, this.config, this.$http, src, arg)));
236
- return imgList.filter(Boolean).map(img => `<img src="${img}"/>`).join("");
237
- }
238
- async processOnlyImageTemplate(item, arg, html) {
239
- let imgList = [];
240
- html('img').map((key, i) => imgList.push(i.attribs.src));
241
- imgList = await Promise.all([...new Set(imgList)].map(async (src) => await (0, media_1.getImageUrl)(this.ctx, this.config, this.$http, src, arg)));
242
- return imgList.filter(Boolean).map(img => `<img src="${img}"/>`).join("");
243
- }
244
- async processDefaultTemplate(item, arg, html, parseContent) {
245
- item.description = parseContent((0, template_1.getDefaultTemplate)(this.config, arg.bodyWidth, arg.bodyPadding, arg.bodyFontSize || this.config.template?.bodyFontSize), { ...item, arg });
246
- (0, logger_1.debug)(this.config, item.description, 'description');
247
- // 如果有AI摘要,在图片渲染前将其注入到HTML中
248
- const hasAiSummary = item.aiSummary && item.aiSummary.trim();
249
- if (hasAiSummary && (this.config.basic?.imageMode === 'base64' || this.config.basic?.imageMode === 'File' || this.config.basic?.imageMode === 'assets')) {
250
- // 将markdown转换为HTML
251
- const aiSummaryHtml = await (0, marked_1.marked)(item.aiSummary);
252
- const aiSummarySection = `
253
- <div class="ai-summary-section mb-6">
254
- <div class="flex items-start gap-3 mb-3">
255
- <div class="mt-0.5 w-6 h-6 rounded-md bg-primary/10 flex flex-shrink-0 items-center justify-center">
256
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 text-primary">
257
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
258
- </svg>
259
- </div>
260
- <h3 class="text-sm font-bold text-slate-700">AI 摘要</h3>
261
- </div>
262
- <div class="pl-9 prose prose-slate prose-sm max-w-none">
263
- ${aiSummaryHtml}
264
- </div>
265
- </div>
266
- <div class="border-t border-slate-100 my-6"></div>
267
- `;
268
- item.description = aiSummarySection + item.description;
114
+ const sep = this.config.ai.separator || '----------------';
115
+ msg = this.config.ai.placement === 'bottom'
116
+ ? `${msg}\n${sep}\n${formattedAiSummary}`
117
+ : `${formattedAiSummary}\n${sep}\n${msg}`;
269
118
  }
270
- html = cheerio.load(item.description);
271
- if (arg?.proxyAgent?.enabled) {
272
- 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());
273
- }
274
- html('img').attr('style', 'object-fit:scale-down;max-width:100%;');
275
- (0, logger_1.debug)(this.config, `当前 imageMode: ${this.config.basic?.imageMode}`, 'imageMode', 'info');
276
- let msg = await this.renderImage(html.html(), arg);
277
- return msg;
278
- }
279
- async processOnlyDescriptionTemplate(item, arg, html, parseContent) {
280
- item.description = parseContent((0, template_1.getDescriptionTemplate)(this.config, arg.bodyWidth, arg.bodyPadding, arg.bodyFontSize || this.config.template?.bodyFontSize), { ...item, arg });
281
- (0, logger_1.debug)(this.config, item.description, 'description');
282
- // 如果有AI摘要,在图片渲染前将其注入到HTML中
283
- const hasAiSummary = item.aiSummary && item.aiSummary.trim();
284
- if (hasAiSummary && (this.config.basic?.imageMode === 'base64' || this.config.basic?.imageMode === 'File' || this.config.basic?.imageMode === 'assets')) {
285
- // 将markdown转换为HTML
286
- const aiSummaryHtml = await (0, marked_1.marked)(item.aiSummary);
287
- const aiSummarySection = `
288
- <div class="ai-summary-section mb-6">
289
- <div class="flex items-start gap-3 mb-3">
290
- <div class="mt-0.5 w-6 h-6 rounded-md bg-primary/10 flex flex-shrink-0 items-center justify-center">
291
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 text-primary">
292
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
293
- </svg>
294
- </div>
295
- <h3 class="text-sm font-bold text-slate-700">AI 摘要</h3>
296
- </div>
297
- <div class="pl-9 prose prose-slate prose-sm max-w-none" style="color: #475569; line-height: 1.6;">
298
- ${aiSummaryHtml}
299
- </div>
300
- </div>
301
- <div style="border-top: 1px solid #e2e8f0; margin: 24px 0;"></div>
302
- `;
303
- item.description = aiSummarySection + item.description;
304
- }
305
- html = cheerio.load(item.description);
306
- if (arg?.proxyAgent?.enabled) {
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());
308
- }
309
- html('img').attr('style', 'object-fit:scale-down;max-width:100%;');
310
- let msg = await this.renderImage(html.html(), arg);
311
- return msg;
312
- }
313
- async processLinkTemplate(item, arg) {
314
- let html = cheerio.load(item.description);
315
- let src = html('a')[0].attribs.href;
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
- }
328
- let html2 = cheerio.load((await this.$http(src, arg)).data);
329
- if (arg?.proxyAgent?.enabled) {
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());
331
- }
332
- html2('img').attr('style', 'object-fit:scale-down;max-width:100%;');
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);
119
+ (0, logger_1.debug)(this.config, msg, 'parse:msg', 'info');
338
120
  return msg;
339
121
  }
340
- async processVideos(html, arg, videoList) {
341
- await Promise.all(html('video').map(async (v, i) => videoList.push([
342
- await (0, media_1.getVideoUrl)(this.ctx, this.config, this.$http, i.attribs.src, arg, true, i),
343
- (i.attribs.poster && this.config.basic?.usePoster) ? await (0, media_1.getImageUrl)(this.ctx, this.config, this.$http, i.attribs.poster, arg, true) : ""
344
- ])).get());
345
- }
346
- formatVideoList(videoList) {
347
- return videoList.filter(([src]) => src).map(([src, poster]) => {
348
- // href 模式:返回视频链接文本
349
- if (src.startsWith('__VIDEO_LINK__:')) {
350
- const videoUrl = src.replace('__VIDEO_LINK__:', '');
351
- return `\n🎬 视频: ${videoUrl}\n`;
352
- }
353
- // 其他模式:创建 video 元素
354
- return (0, koishi_1.h)('video', { src, poster });
355
- }).join('');
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
- }
397
122
  }
398
123
  exports.RssItemProcessor = RssItemProcessor;
@@ -0,0 +1,25 @@
1
+ import { Config } from '../types';
2
+ import { QueueTaskContent } from './notification-queue-types';
3
+ declare const DEFAULT_QUEUE_BACKOFF_DELAYS: number[];
4
+ declare const DEFAULT_QUEUE_BATCH_SIZE = 10;
5
+ declare const DEFAULT_QUEUE_MAX_RETRIES = 5;
6
+ declare const DEFAULT_QUEUE_PROCESS_INTERVAL_SECONDS = 30;
7
+ declare const DEFAULT_QUEUE_CLEANUP_HOURS = 24;
8
+ export interface QueueRuntimeConfig {
9
+ batchSize: number;
10
+ maxRetries: number;
11
+ processIntervalSeconds: number;
12
+ cleanupHours: number;
13
+ }
14
+ export type QueueErrorAction = 'FAILED' | 'DOWNGRADE' | 'RETRY';
15
+ export interface QueueErrorClassification {
16
+ action: QueueErrorAction;
17
+ normalizedError: Error;
18
+ }
19
+ export declare function getQueueRuntimeConfig(config?: Config): QueueRuntimeConfig;
20
+ export declare function isFatalQueueError(error: any): boolean;
21
+ export declare function isQueueDowngradeError(error: any): boolean;
22
+ export declare function classifyQueueError(error: unknown, content?: Pick<QueueTaskContent, 'isDowngraded'>): QueueErrorClassification;
23
+ export declare function shouldStopRetrying(retryCount: number, maxRetries: number): boolean;
24
+ export declare function getRetryDelaySeconds(retryCount: number, backoffDelays?: number[]): number;
25
+ export { DEFAULT_QUEUE_BACKOFF_DELAYS, DEFAULT_QUEUE_BATCH_SIZE, DEFAULT_QUEUE_MAX_RETRIES, DEFAULT_QUEUE_PROCESS_INTERVAL_SECONDS, DEFAULT_QUEUE_CLEANUP_HOURS, };
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_QUEUE_CLEANUP_HOURS = exports.DEFAULT_QUEUE_PROCESS_INTERVAL_SECONDS = exports.DEFAULT_QUEUE_MAX_RETRIES = exports.DEFAULT_QUEUE_BATCH_SIZE = exports.DEFAULT_QUEUE_BACKOFF_DELAYS = void 0;
4
+ exports.getQueueRuntimeConfig = getQueueRuntimeConfig;
5
+ exports.isFatalQueueError = isFatalQueueError;
6
+ exports.isQueueDowngradeError = isQueueDowngradeError;
7
+ exports.classifyQueueError = classifyQueueError;
8
+ exports.shouldStopRetrying = shouldStopRetrying;
9
+ exports.getRetryDelaySeconds = getRetryDelaySeconds;
10
+ const error_handler_1 = require("../utils/error-handler");
11
+ const DEFAULT_QUEUE_BACKOFF_DELAYS = [10, 30, 60, 300, 600];
12
+ exports.DEFAULT_QUEUE_BACKOFF_DELAYS = DEFAULT_QUEUE_BACKOFF_DELAYS;
13
+ const DEFAULT_QUEUE_BATCH_SIZE = 10;
14
+ exports.DEFAULT_QUEUE_BATCH_SIZE = DEFAULT_QUEUE_BATCH_SIZE;
15
+ const DEFAULT_QUEUE_MAX_RETRIES = 5;
16
+ exports.DEFAULT_QUEUE_MAX_RETRIES = DEFAULT_QUEUE_MAX_RETRIES;
17
+ const DEFAULT_QUEUE_PROCESS_INTERVAL_SECONDS = 30;
18
+ exports.DEFAULT_QUEUE_PROCESS_INTERVAL_SECONDS = DEFAULT_QUEUE_PROCESS_INTERVAL_SECONDS;
19
+ const DEFAULT_QUEUE_CLEANUP_HOURS = 24;
20
+ exports.DEFAULT_QUEUE_CLEANUP_HOURS = DEFAULT_QUEUE_CLEANUP_HOURS;
21
+ function clampInteger(value, min, max, fallback) {
22
+ if (!Number.isFinite(value)) {
23
+ return fallback;
24
+ }
25
+ return Math.min(max, Math.max(min, Math.floor(value)));
26
+ }
27
+ function getQueueRuntimeConfig(config) {
28
+ const queueConfig = config?.queue;
29
+ return {
30
+ batchSize: clampInteger(queueConfig?.batchSize, 1, 50, DEFAULT_QUEUE_BATCH_SIZE),
31
+ maxRetries: clampInteger(queueConfig?.maxRetries, 0, 20, DEFAULT_QUEUE_MAX_RETRIES),
32
+ processIntervalSeconds: clampInteger(queueConfig?.processInterval, 5, 3600, DEFAULT_QUEUE_PROCESS_INTERVAL_SECONDS),
33
+ cleanupHours: clampInteger(queueConfig?.cleanupHours, 1, 720, DEFAULT_QUEUE_CLEANUP_HOURS),
34
+ };
35
+ }
36
+ function isFatalQueueError(error) {
37
+ const errorCode = error?.code?.toString?.() || error?.retcode?.toString?.();
38
+ if (errorCode === 'UnknownGroup' || errorCode === 'GROUP_NOT_FOUND') {
39
+ return true;
40
+ }
41
+ if (errorCode === 'UserBlock' || errorCode === 'BANNED') {
42
+ return true;
43
+ }
44
+ if (errorCode === 'PermissionDenied' || errorCode === 'NO_PERMISSION') {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ function isQueueDowngradeError(error) {
50
+ const errorCode = error?.code?.toString?.() || error?.retcode?.toString?.();
51
+ const errorMessage = error?.message?.toString?.() || '';
52
+ return errorCode === '1200' || errorMessage.includes('1200') || Boolean(error?.requiresDowngrade);
53
+ }
54
+ function classifyQueueError(error, content) {
55
+ const normalizedError = (0, error_handler_1.normalizeError)(error);
56
+ if (!content?.isDowngraded && isQueueDowngradeError(error)) {
57
+ return {
58
+ action: 'DOWNGRADE',
59
+ normalizedError,
60
+ };
61
+ }
62
+ if (isFatalQueueError(error)) {
63
+ return {
64
+ action: 'FAILED',
65
+ normalizedError,
66
+ };
67
+ }
68
+ return {
69
+ action: 'RETRY',
70
+ normalizedError,
71
+ };
72
+ }
73
+ function shouldStopRetrying(retryCount, maxRetries) {
74
+ return retryCount >= Math.max(0, maxRetries);
75
+ }
76
+ function getRetryDelaySeconds(retryCount, backoffDelays = DEFAULT_QUEUE_BACKOFF_DELAYS) {
77
+ return backoffDelays[retryCount] || backoffDelays[backoffDelays.length - 1];
78
+ }
@@ -0,0 +1,20 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from '../types';
3
+ import { createDebugWithContext } from '../utils/logger';
4
+ import { QueueTask, QueueTaskContent } from './notification-queue-types';
5
+ type QueueDebugFn = ReturnType<typeof createDebugWithContext>;
6
+ interface NotificationQueueSenderDeps {
7
+ ctx: Context;
8
+ config: Config;
9
+ createTaskDebug: (task: Partial<QueueTask>) => QueueDebugFn;
10
+ buildTaskLogContext: (task: Partial<QueueTask>) => Record<string, any>;
11
+ }
12
+ export declare function downgradeQueueMessage(content: QueueTaskContent): QueueTaskContent;
13
+ export declare class NotificationQueueSender {
14
+ private deps;
15
+ constructor(deps: NotificationQueueSenderDeps);
16
+ sendMessage(task: QueueTask): Promise<void>;
17
+ downgradeMessage(content: QueueTaskContent): Promise<QueueTaskContent>;
18
+ cacheMessage(task: QueueTask): Promise<void>;
19
+ }
20
+ export {};
@@ -0,0 +1,118 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NotificationQueueSender = void 0;
37
+ exports.downgradeQueueMessage = downgradeQueueMessage;
38
+ const error_handler_1 = require("../utils/error-handler");
39
+ const error_tracker_1 = require("../utils/error-tracker");
40
+ const notification_queue_retry_1 = require("./notification-queue-retry");
41
+ function downgradeQueueMessage(content) {
42
+ if (content.isDowngraded) {
43
+ return {
44
+ ...content,
45
+ isDowngraded: true,
46
+ };
47
+ }
48
+ const downgradedMessage = content.message.replace(/<video[^>]*>.*?<\/video>/gis, (match) => {
49
+ const srcMatch = match.match(/src=["']([^"']+)["']/);
50
+ if (srcMatch) {
51
+ return `\n🎬 视频: ${srcMatch[1]}\n`;
52
+ }
53
+ return '\n[视频不支持]\n';
54
+ });
55
+ return {
56
+ ...content,
57
+ message: downgradedMessage,
58
+ isDowngraded: true,
59
+ };
60
+ }
61
+ class NotificationQueueSender {
62
+ deps;
63
+ constructor(deps) {
64
+ this.deps = deps;
65
+ }
66
+ async sendMessage(task) {
67
+ const { guildId, platform, content } = task;
68
+ const target = `${platform}:${guildId}`;
69
+ const taskDebug = this.deps.createTaskDebug(task);
70
+ try {
71
+ await this.deps.ctx.broadcast([target], content.message);
72
+ taskDebug(`消息发送成功: ${target}`, 'queue', 'details');
73
+ }
74
+ catch (sendError) {
75
+ if ((0, notification_queue_retry_1.isQueueDowngradeError)(sendError) && !content.isDowngraded) {
76
+ taskDebug('检测到 OneBot 1200 错误,尝试降级处理', 'queue', 'info', { errorCode: '1200' });
77
+ }
78
+ throw sendError;
79
+ }
80
+ }
81
+ async downgradeMessage(content) {
82
+ return downgradeQueueMessage(content);
83
+ }
84
+ async cacheMessage(task) {
85
+ if (!this.deps.config.cache?.enabled) {
86
+ return;
87
+ }
88
+ const taskDebug = this.deps.createTaskDebug(task);
89
+ const { getMessageCache } = await Promise.resolve().then(() => __importStar(require('../utils/message-cache')));
90
+ const cache = getMessageCache();
91
+ if (!cache) {
92
+ return;
93
+ }
94
+ try {
95
+ await cache.addMessage({
96
+ rssId: task.rssId,
97
+ guildId: task.guildId,
98
+ platform: task.platform,
99
+ title: task.content.title || '',
100
+ content: task.content.description || '',
101
+ link: task.content.link || '',
102
+ pubDate: task.content.pubDate || new Date(),
103
+ imageUrl: task.content.imageUrl || '',
104
+ videoUrl: '',
105
+ finalMessage: task.content.message,
106
+ });
107
+ }
108
+ catch (err) {
109
+ const normalizedError = (0, error_handler_1.normalizeError)(err);
110
+ taskDebug(`缓存消息失败: ${normalizedError.message}`, 'cache', 'info');
111
+ (0, error_tracker_1.trackError)(normalizedError, {
112
+ ...this.deps.buildTaskLogContext(task),
113
+ operation: 'cacheMessage',
114
+ });
115
+ }
116
+ }
117
+ }
118
+ exports.NotificationQueueSender = NotificationQueueSender;