@anyul/koishi-plugin-rss 5.2.3 → 5.2.41

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
@@ -1,46 +1,12 @@
1
1
  "use strict";
2
- /**
3
- * 消息发送队列管理器
4
- * 实现可靠的消息推送,支持重试、降级和错误处理
5
- */
6
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
- if (k2 === undefined) k2 = k;
8
- var desc = Object.getOwnPropertyDescriptor(m, k);
9
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
- desc = { enumerable: true, get: function() { return m[k]; } };
11
- }
12
- Object.defineProperty(o, k2, desc);
13
- }) : (function(o, m, k, k2) {
14
- if (k2 === undefined) k2 = k;
15
- o[k2] = m[k];
16
- }));
17
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
- Object.defineProperty(o, "default", { enumerable: true, value: v });
19
- }) : function(o, v) {
20
- o["default"] = v;
21
- });
22
- var __importStar = (this && this.__importStar) || (function () {
23
- var ownKeys = function(o) {
24
- ownKeys = Object.getOwnPropertyNames || function (o) {
25
- var ar = [];
26
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
- return ar;
28
- };
29
- return ownKeys(o);
30
- };
31
- return function (mod) {
32
- if (mod && mod.__esModule) return mod;
33
- var result = {};
34
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
- __setModuleDefault(result, mod);
36
- return result;
37
- };
38
- })();
39
2
  Object.defineProperty(exports, "__esModule", { value: true });
40
3
  exports.NotificationQueueManager = void 0;
41
4
  const error_handler_1 = require("../utils/error-handler");
42
5
  const error_tracker_1 = require("../utils/error-tracker");
43
6
  const logger_1 = require("../utils/logger");
7
+ const notification_queue_retry_1 = require("./notification-queue-retry");
8
+ const notification_queue_sender_1 = require("./notification-queue-sender");
9
+ const notification_queue_store_1 = require("./notification-queue-store");
44
10
  /**
45
11
  * 消息发送队列管理器
46
12
  */
@@ -48,13 +14,27 @@ class NotificationQueueManager {
48
14
  ctx;
49
15
  config;
50
16
  processing = false;
51
- maxRetries = 5;
52
- batchSize = 10;
53
- // 指数退避时间(秒):10s, 30s, 1m, 5m, 10m
54
- backoffDelays = [10, 30, 60, 300, 600];
17
+ recovered = false;
18
+ batchSize;
19
+ maxRetries;
20
+ cleanupHours;
21
+ store;
22
+ sender;
23
+ backoffDelays = notification_queue_retry_1.DEFAULT_QUEUE_BACKOFF_DELAYS;
55
24
  constructor(ctx, config) {
56
25
  this.ctx = ctx;
57
26
  this.config = config;
27
+ const runtimeConfig = (0, notification_queue_retry_1.getQueueRuntimeConfig)(config);
28
+ this.batchSize = runtimeConfig.batchSize;
29
+ this.maxRetries = runtimeConfig.maxRetries;
30
+ this.cleanupHours = runtimeConfig.cleanupHours;
31
+ this.store = new notification_queue_store_1.NotificationQueueStore(ctx, this.batchSize);
32
+ this.sender = new notification_queue_sender_1.NotificationQueueSender({
33
+ ctx,
34
+ config,
35
+ createTaskDebug: task => this.createTaskDebug(task),
36
+ buildTaskLogContext: task => this.buildTaskLogContext(task),
37
+ });
58
38
  }
59
39
  buildTaskLogContext(task) {
60
40
  const context = {
@@ -80,18 +60,32 @@ class NotificationQueueManager {
80
60
  * 添加任务到队列
81
61
  */
82
62
  async addTask(task) {
83
- const queueTask = {
84
- ...task,
85
- status: 'PENDING',
86
- retryCount: 0,
87
- createdAt: new Date(),
88
- updatedAt: new Date()
89
- };
63
+ const { task: queueTask, created } = await this.store.createTask(task);
90
64
  const taskDebug = this.createTaskDebug(queueTask);
91
- await this.ctx.database.create('rss_notification_queue', queueTask);
65
+ if (!created) {
66
+ taskDebug(`检测到重复任务,跳过入队: [${task.rssId}] ${task.content.title || task.uid}`, 'queue', 'info', {
67
+ duplicate: true,
68
+ });
69
+ return queueTask;
70
+ }
92
71
  taskDebug(`任务已加入队列: [${task.rssId}] ${task.content.title}`, 'queue', 'info');
93
72
  return queueTask;
94
73
  }
74
+ isProcessing() {
75
+ return this.processing;
76
+ }
77
+ async ensureRecovered() {
78
+ if (this.recovered) {
79
+ return;
80
+ }
81
+ const recoveredCount = await this.store.recoverRetryTasksWithoutNextRetryTime();
82
+ this.recovered = true;
83
+ if (recoveredCount > 0) {
84
+ (0, logger_1.debug)(this.config, `已恢复 ${recoveredCount} 个缺少下次重试时间的任务`, 'queue', 'info', {
85
+ recoveredCount,
86
+ });
87
+ }
88
+ }
95
89
  /**
96
90
  * 处理队列中的任务
97
91
  */
@@ -102,13 +96,12 @@ class NotificationQueueManager {
102
96
  }
103
97
  this.processing = true;
104
98
  try {
105
- // 1. 查找待处理任务
106
- const tasks = await this.getPendingTasks();
99
+ await this.ensureRecovered();
100
+ const tasks = await this.store.getPendingTasks();
107
101
  if (tasks.length === 0) {
108
102
  return;
109
103
  }
110
104
  (0, logger_1.debug)(this.config, `开始处理 ${tasks.length} 个待发送任务`, 'queue', 'info', { taskCount: tasks.length });
111
- // 2. 逐个处理任务
112
105
  for (const task of tasks) {
113
106
  await this.processTask(task);
114
107
  }
@@ -122,22 +115,6 @@ class NotificationQueueManager {
122
115
  this.processing = false;
123
116
  }
124
117
  }
125
- /**
126
- * 获取待处理任务
127
- */
128
- async getPendingTasks() {
129
- const now = new Date();
130
- // 获取所有 PENDING 状态的任务
131
- const pendingTasks = await this.ctx.database.get('rss_notification_queue', { status: 'PENDING' }, { limit: this.batchSize });
132
- // 获取到达重试时间的 RETRY 状态任务
133
- const retryTasks = await this.ctx.database.get('rss_notification_queue', { status: 'RETRY' }, { limit: this.batchSize });
134
- // 过滤出到达重试时间的任务
135
- const readyRetryTasks = retryTasks.filter(task => task.nextRetryTime && new Date(task.nextRetryTime) <= now);
136
- // 合并并按创建时间排序
137
- return [...pendingTasks, ...readyRetryTasks]
138
- .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
139
- .slice(0, this.batchSize);
140
- }
141
118
  /**
142
119
  * 处理单个任务
143
120
  */
@@ -145,75 +122,56 @@ class NotificationQueueManager {
145
122
  const taskDebug = this.createTaskDebug(task);
146
123
  taskDebug(`处理任务 [${task.rssId}] ${task.content.title} (重试${task.retryCount}次)`, 'queue', 'details');
147
124
  try {
148
- // 尝试发送消息
149
- await this.sendMessage(task);
150
- // 发送成功:标记为 SUCCESS
151
- await this.markTaskSuccess(task.id);
125
+ await this.sender.sendMessage(task);
126
+ await this.store.markTaskSuccess(task.id);
152
127
  taskDebug(`✓ 任务发送成功: [${task.rssId}] ${task.content.title}`, 'queue', 'info');
153
- // 写入缓存
154
- await this.cacheMessage(task);
128
+ await this.sender.cacheMessage(task);
155
129
  }
156
130
  catch (error) {
157
- // 进入错误处理流程
158
131
  await this.handleSendError(task, error);
159
132
  }
160
133
  }
161
- /**
162
- * 发送消息(带降级机制)
163
- */
164
- async sendMessage(task) {
165
- const { guildId, platform, content } = task;
166
- const target = `${platform}:${guildId}`;
167
- const taskDebug = this.createTaskDebug(task);
168
- try {
169
- // 第一次尝试:发送原始消息
170
- await this.ctx.broadcast([target], content.message);
171
- taskDebug(`消息发送成功: ${target}`, 'queue', 'details');
172
- }
173
- catch (sendError) {
174
- // OneBot retcode 1200: 不支持的消息格式(通常是视频)
175
- const isOneBot1200 = sendError.code?.toString?.() === '1200' || sendError.message?.includes('1200');
176
- if (isOneBot1200 && !content.isDowngraded) {
177
- taskDebug(`检测到 OneBot 1200 错误,尝试降级处理`, 'queue', 'info', { errorCode: '1200' });
178
- throw { ...sendError, isMediaError: true, requiresDowngrade: true };
179
- }
180
- throw sendError;
181
- }
182
- }
183
134
  /**
184
135
  * 处理发送错误
185
136
  */
186
137
  async handleSendError(task, error) {
187
138
  const taskDebug = this.createTaskDebug(task);
188
- const normalizedError = (0, error_handler_1.normalizeError)(error);
139
+ const classification = (0, notification_queue_retry_1.classifyQueueError)(error, task.content);
140
+ const normalizedError = classification.normalizedError;
189
141
  const errorMsg = normalizedError.message || 'Unknown error';
190
142
  (0, error_tracker_1.trackError)(normalizedError, {
191
143
  ...this.buildTaskLogContext(task),
192
144
  failReason: errorMsg,
193
- requiresDowngrade: Boolean(error?.requiresDowngrade),
145
+ queueErrorAction: classification.action,
194
146
  });
195
- // 1. 永久性错误 (Fatal) - 不需要重试
196
- if (this.isFatalError(error)) {
197
- await this.markTaskFailed(task.id, errorMsg);
147
+ if (classification.action === 'FAILED') {
148
+ await this.store.markTaskFailed(task.id, errorMsg);
198
149
  taskDebug(`✗ 永久性失败,放弃重试: [${task.rssId}] ${task.content.title} - ${errorMsg}`, 'queue', 'error', {
199
150
  fatal: true,
200
151
  failReason: errorMsg,
201
152
  });
202
153
  return;
203
154
  }
204
- // 2. 降级重试 (Downgrade) - 针对媒体格式错误
205
- if (error.requiresDowngrade && !task.content.isDowngraded) {
155
+ if (classification.action === 'DOWNGRADE') {
206
156
  const downgradedContent = await this.downgradeMessage(task.content);
207
- await this.updateTaskForDowngrade(task, downgradedContent);
157
+ await this.store.updateTaskForDowngrade(task, downgradedContent);
208
158
  taskDebug(`→ 消息已降级,立即重试: [${task.rssId}] ${task.content.title}`, 'queue', 'info', {
209
- requiresDowngrade: true,
159
+ queueErrorAction: classification.action,
210
160
  });
211
161
  return;
212
162
  }
213
- // 3. 暂时性错误 (Transient) - 使用指数退避
214
- const delay = this.backoffDelays[task.retryCount] || this.backoffDelays[this.backoffDelays.length - 1];
163
+ if ((0, notification_queue_retry_1.shouldStopRetrying)(task.retryCount, this.maxRetries)) {
164
+ const maxRetryMessage = `超过最大重试次数(${this.maxRetries}): ${errorMsg}`;
165
+ await this.store.markTaskFailed(task.id, maxRetryMessage);
166
+ taskDebug(`✗ 已达到最大重试次数,任务失败: [${task.rssId}] ${task.content.title} - ${errorMsg}`, 'queue', 'error', {
167
+ maxRetries: this.maxRetries,
168
+ failReason: maxRetryMessage,
169
+ });
170
+ return;
171
+ }
172
+ const delay = (0, notification_queue_retry_1.getRetryDelaySeconds)(task.retryCount, this.backoffDelays);
215
173
  const nextTime = new Date(Date.now() + delay * 1000);
216
- await this.markTaskRetry(task, nextTime, errorMsg);
174
+ await this.store.markTaskRetry(task, nextTime, errorMsg);
217
175
  taskDebug(`→ 任务将在 ${Math.ceil(delay / 60)} 分钟后重试: [${task.rssId}] ${task.content.title}`, 'queue', 'info', {
218
176
  nextRetryTime: nextTime.toISOString(),
219
177
  failReason: errorMsg,
@@ -223,177 +181,42 @@ class NotificationQueueManager {
223
181
  * 判断是否为永久性错误
224
182
  */
225
183
  isFatalError(error) {
226
- const errorCode = error.code || error.retcode;
227
- // 群组不存在 / 账号不在群内
228
- if (errorCode === 'UnknownGroup' || errorCode === 'GROUP_NOT_FOUND') {
229
- return true;
230
- }
231
- // 账号被封禁 / 被拉黑
232
- if (errorCode === 'UserBlock' || errorCode === 'BANNED') {
233
- return true;
234
- }
235
- // 权限不足
236
- if (errorCode === 'PermissionDenied' || errorCode === 'NO_PERMISSION') {
237
- return true;
238
- }
239
- // 超过最大重试次数
240
- // 这个判断在调用处处理
241
- return false;
184
+ return (0, notification_queue_retry_1.isFatalQueueError)(error);
242
185
  }
243
186
  /**
244
187
  * 降级消息(移除媒体元素)
245
188
  */
246
189
  async downgradeMessage(content) {
247
- // 移除 video 元素,保留视频链接
248
- let downgradedMessage = content.message.replace(/<video[^>]*>.*?<\/video>/gis, (match) => {
249
- const srcMatch = match.match(/src=["']([^"']+)["']/);
250
- if (srcMatch) {
251
- return `\n🎬 视频: ${srcMatch[1]}\n`;
252
- }
253
- return '\n[视频不支持]\n';
254
- });
255
- // 移除 img 元素,保留图片链接(可选)
256
- // downgradedMessage = downgradedMessage.replace(/<img[^>]*>/gis, (match: string) => {
257
- // const srcMatch = match.match(/src=["']([^"']+)["']/)
258
- // if (srcMatch) {
259
- // return `\n🖼️ 图片: ${srcMatch[1]}\n`
260
- // }
261
- // return '\n[图片不支持]\n'
262
- // })
263
- return {
264
- ...content,
265
- message: downgradedMessage,
266
- isDowngraded: true
267
- };
268
- }
269
- /**
270
- * 标记任务为成功
271
- */
272
- async markTaskSuccess(taskId) {
273
- await this.ctx.database.set('rss_notification_queue', { id: taskId }, {
274
- status: 'SUCCESS',
275
- updatedAt: new Date()
276
- });
277
- // 可选:定期清理成功任务,避免数据库膨胀
278
- // await this.ctx.database.remove(('rss_notification_queue' as any), { id: taskId })
279
- }
280
- /**
281
- * 标记任务为重试
282
- */
283
- async markTaskRetry(task, nextTime, reason) {
284
- await this.ctx.database.set('rss_notification_queue', { id: task.id }, {
285
- status: 'RETRY',
286
- nextRetryTime: nextTime,
287
- retryCount: (task.retryCount || 0) + 1,
288
- failReason: reason,
289
- updatedAt: new Date()
290
- });
291
- }
292
- /**
293
- * 更新任务为降级重试
294
- */
295
- async updateTaskForDowngrade(task, newContent) {
296
- await this.ctx.database.set('rss_notification_queue', { id: task.id }, {
297
- content: newContent,
298
- status: 'RETRY',
299
- nextRetryTime: new Date(), // 立即重试
300
- retryCount: (task.retryCount || 0) + 1,
301
- updatedAt: new Date()
302
- });
303
- }
304
- /**
305
- * 标记任务为失败
306
- */
307
- async markTaskFailed(taskId, reason) {
308
- await this.ctx.database.set('rss_notification_queue', { id: taskId }, {
309
- status: 'FAILED',
310
- failReason: reason,
311
- updatedAt: new Date()
312
- });
313
- }
314
- /**
315
- * 缓存成功发送的消息
316
- */
317
- async cacheMessage(task) {
318
- if (!this.config.cache?.enabled) {
319
- return;
320
- }
321
- const taskDebug = this.createTaskDebug(task);
322
- const { getMessageCache } = await Promise.resolve().then(() => __importStar(require('../utils/message-cache')));
323
- const cache = getMessageCache();
324
- if (!cache) {
325
- return;
326
- }
327
- try {
328
- await cache.addMessage({
329
- rssId: task.rssId,
330
- guildId: task.guildId,
331
- platform: task.platform,
332
- title: task.content.title || '',
333
- content: task.content.description || '',
334
- link: task.content.link || '',
335
- pubDate: task.content.pubDate || new Date(),
336
- imageUrl: task.content.imageUrl || '',
337
- videoUrl: '',
338
- finalMessage: task.content.message
339
- });
340
- }
341
- catch (err) {
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
- });
348
- }
190
+ return this.sender.downgradeMessage(content);
349
191
  }
350
192
  /**
351
193
  * 获取队列统计信息
352
194
  */
353
195
  async getStats() {
354
- const allTasks = await this.ctx.database.get('rss_notification_queue', {});
355
- return {
356
- pending: allTasks.filter((t) => t.status === 'PENDING').length,
357
- retry: allTasks.filter((t) => t.status === 'RETRY').length,
358
- failed: allTasks.filter((t) => t.status === 'FAILED').length,
359
- success: allTasks.filter((t) => t.status === 'SUCCESS').length
360
- };
196
+ return this.store.getStats();
361
197
  }
362
198
  /**
363
199
  * 重试失败的任务
364
200
  */
365
201
  async retryFailedTasks(taskId) {
366
- const where = taskId ? { id: taskId } : { status: 'FAILED' };
367
- const tasks = await this.ctx.database.get('rss_notification_queue', where);
368
- const failedTasks = tasks.filter(task => task.status === 'FAILED');
369
- for (const task of failedTasks) {
370
- await this.ctx.database.set('rss_notification_queue', { id: task.id }, {
371
- status: 'PENDING',
372
- retryCount: 0,
373
- failReason: null,
374
- updatedAt: new Date()
375
- });
376
- }
377
- (0, logger_1.debug)(this.config, `已重置 ${failedTasks.length} 个失败任务为 PENDING 状态`, 'queue', 'info', {
378
- resetCount: failedTasks.length,
202
+ const resetCount = await this.store.retryFailedTasks(taskId);
203
+ (0, logger_1.debug)(this.config, `已重置 ${resetCount} 个失败任务为 PENDING 状态`, 'queue', 'info', {
204
+ resetCount,
379
205
  taskId,
380
206
  });
381
- return failedTasks.length;
207
+ return resetCount;
382
208
  }
383
209
  /**
384
210
  * 清理旧的成功任务
385
211
  */
386
- async cleanupSuccessTasks(olderThanHours = 24) {
387
- const cutoffTime = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
388
- const tasks = await this.ctx.database.get('rss_notification_queue', { status: 'SUCCESS', updatedAt: { $lt: cutoffTime } });
389
- for (const task of tasks) {
390
- await this.ctx.database.remove('rss_notification_queue', { id: task.id });
391
- }
392
- (0, logger_1.debug)(this.config, `已清理 ${tasks.length} 个旧的成功任务`, 'queue', 'info', {
393
- cleanupCount: tasks.length,
394
- olderThanHours,
212
+ async cleanupSuccessTasks(olderThanHours) {
213
+ const cleanupHours = olderThanHours ?? this.cleanupHours;
214
+ const cleanupCount = await this.store.cleanupSuccessTasks(cleanupHours);
215
+ (0, logger_1.debug)(this.config, `已清理 ${cleanupCount} 个旧的成功任务`, 'queue', 'info', {
216
+ cleanupCount,
217
+ olderThanHours: cleanupHours,
395
218
  });
396
- return tasks.length;
219
+ return cleanupCount;
397
220
  }
398
221
  }
399
222
  exports.NotificationQueueManager = NotificationQueueManager;
@@ -0,0 +1,3 @@
1
+ import { SearchResponse } from './search-types';
2
+ export declare function formatSearchResults(response: SearchResponse): string;
3
+ export declare function buildPromptWithSearchContext(originalPrompt: string, searchResults: SearchResponse, searchQuery: string): string;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatSearchResults = formatSearchResults;
4
+ exports.buildPromptWithSearchContext = buildPromptWithSearchContext;
5
+ function formatSearchResults(response) {
6
+ if (!response.success || response.results.length === 0) {
7
+ return '';
8
+ }
9
+ const lines = [];
10
+ lines.push(`\n联网搜索结果 (${response.engine}):\n`);
11
+ response.results.forEach((result, index) => {
12
+ lines.push(`${index + 1}. ${result.title}`);
13
+ lines.push(` 链接: ${result.url}`);
14
+ if (result.snippet) {
15
+ lines.push(` 摘要: ${result.snippet}`);
16
+ }
17
+ if (result.publishedDate) {
18
+ lines.push(` 发布时间: ${result.publishedDate}`);
19
+ }
20
+ lines.push('');
21
+ });
22
+ return lines.join('\n');
23
+ }
24
+ function buildPromptWithSearchContext(originalPrompt, searchResults, searchQuery) {
25
+ if (!searchResults.success || searchResults.results.length === 0) {
26
+ return originalPrompt;
27
+ }
28
+ const formattedResults = formatSearchResults(searchResults);
29
+ return `${originalPrompt}
30
+
31
+ ${formattedResults}
32
+
33
+ 【搜索结果使用原则】:
34
+ 1. 若 RSS 原始内容残缺,请使用以上搜索结果进行补全。
35
+ 2. 搜索结果仅作为背景参考,若搜索结果中的人物、时间、事件与 RSS 原文冲突,**必须以 RSS 原文为准**!请提取并生成一份事实准确、语言简洁流畅的摘要。`;
36
+ }
@@ -0,0 +1,13 @@
1
+ import { Config, SearchConfig } from '../types';
2
+ import { SearchResponse } from './search-types';
3
+ export declare function searchWithTavily(config: Config, query: string, apiKey: string, options?: {
4
+ maxResults?: number;
5
+ searchDepth?: 'basic' | 'advanced';
6
+ includeAnswer?: boolean;
7
+ }): Promise<SearchResponse>;
8
+ export declare function searchWithSearxng(config: Config, query: string, instanceUrl: string, options?: {
9
+ maxResults?: number;
10
+ language?: string;
11
+ categories?: Array<'general' | 'news' | 'images' | 'videos'>;
12
+ }): Promise<SearchResponse>;
13
+ export declare function searchWithVolcengine(config: Config, query: string, baseUrl: string, apiKey: string, model?: string, searchConfig?: SearchConfig): Promise<SearchResponse>;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.searchWithTavily = searchWithTavily;
7
+ exports.searchWithSearxng = searchWithSearxng;
8
+ exports.searchWithVolcengine = searchWithVolcengine;
9
+ const axios_1 = __importDefault(require("axios"));
10
+ const logger_1 = require("../utils/logger");
11
+ const proxy_1 = require("../utils/proxy");
12
+ const search_rotation_1 = require("./search-rotation");
13
+ async function searchWithTavily(config, query, apiKey, options) {
14
+ try {
15
+ (0, logger_1.debug)(config, `使用 Tavily 搜索: ${query}`, 'Search-Tavily', 'info');
16
+ const requestConfig = {
17
+ headers: {
18
+ 'Authorization': `Bearer ${apiKey}`,
19
+ 'Content-Type': 'application/json'
20
+ },
21
+ timeout: config.ai?.timeout || 30000,
22
+ ...(0, proxy_1.buildAxiosProxyConfig)(config)
23
+ };
24
+ const response = await axios_1.default.post('https://api.tavily.com/search', {
25
+ query,
26
+ max_results: options?.maxResults || 5,
27
+ search_depth: options?.searchDepth || 'basic',
28
+ include_answer: options?.includeAnswer !== false,
29
+ include_raw_content: false
30
+ }, requestConfig);
31
+ const results = response.data.results.map(item => ({
32
+ title: item.title,
33
+ url: item.url,
34
+ content: item.content,
35
+ snippet: item.content.substring(0, 200) + '...',
36
+ score: item.score,
37
+ publishedDate: item.published_date,
38
+ source: 'tavily'
39
+ }));
40
+ (0, logger_1.debug)(config, `Tavily 搜索成功,找到 ${results.length} 条结果`, 'Search-Tavily', 'details');
41
+ return {
42
+ success: true,
43
+ results,
44
+ query,
45
+ engine: 'tavily'
46
+ };
47
+ }
48
+ catch (error) {
49
+ const errorMsg = `Tavily 搜索失败: ${error.message}`;
50
+ (0, logger_1.debug)(config, errorMsg, 'Search-Tavily', 'error');
51
+ return {
52
+ success: false,
53
+ results: [],
54
+ query,
55
+ engine: 'tavily',
56
+ error: errorMsg
57
+ };
58
+ }
59
+ }
60
+ async function searchWithSearxng(config, query, instanceUrl, options) {
61
+ try {
62
+ (0, logger_1.debug)(config, `使用 SearXNG 搜索: ${query}`, 'Search-SearXNG', 'info');
63
+ const baseUrl = instanceUrl.replace(/\/+$/, '');
64
+ const requestConfig = {
65
+ timeout: config.ai?.timeout || 30000,
66
+ ...(0, proxy_1.buildAxiosProxyConfig)(config)
67
+ };
68
+ const response = await axios_1.default.get(`${baseUrl}/search`, {
69
+ ...requestConfig,
70
+ params: {
71
+ q: query,
72
+ format: 'json',
73
+ language: options?.language || 'all',
74
+ categories: options?.categories?.join(',') || 'general'
75
+ }
76
+ });
77
+ const maxResults = options?.maxResults || 5;
78
+ const results = response.data.results
79
+ .slice(0, maxResults)
80
+ .map(item => ({
81
+ title: item.title,
82
+ url: item.url,
83
+ content: item.content,
84
+ snippet: item.snippet || item.content.substring(0, 200) + '...',
85
+ score: item.score,
86
+ source: `searxng-${item.engine || 'unknown'}`
87
+ }));
88
+ (0, logger_1.debug)(config, `SearXNG 搜索成功,找到 ${results.length} 条结果`, 'Search-SearXNG', 'details');
89
+ return {
90
+ success: true,
91
+ results,
92
+ query,
93
+ engine: 'searxng'
94
+ };
95
+ }
96
+ catch (error) {
97
+ const errorMsg = `SearXNG 搜索失败: ${error.message}`;
98
+ (0, logger_1.debug)(config, errorMsg, 'Search-SearXNG', 'error');
99
+ return {
100
+ success: false,
101
+ results: [],
102
+ query,
103
+ engine: 'searxng',
104
+ error: errorMsg
105
+ };
106
+ }
107
+ }
108
+ async function searchWithVolcengine(config, query, baseUrl, apiKey, model, searchConfig = {}) {
109
+ const actualModel = model || (0, search_rotation_1.getNextVolcengineModel)(config, searchConfig);
110
+ try {
111
+ (0, logger_1.debug)(config, `使用火山引擎搜索: ${query} (模型: ${actualModel})`, 'Search-Volcengine', 'info');
112
+ const requestConfig = {
113
+ headers: {
114
+ 'Authorization': `Bearer ${apiKey}`,
115
+ 'Content-Type': 'application/json'
116
+ },
117
+ timeout: config.ai?.timeout || 30000,
118
+ ...(0, proxy_1.buildAxiosProxyConfig)(config)
119
+ };
120
+ const response = await axios_1.default.post(`${baseUrl.replace(/\/+$/, '')}/responses`, {
121
+ model: actualModel,
122
+ input: [{ role: 'user', content: query }],
123
+ tools: [{ type: 'web_search' }]
124
+ }, requestConfig);
125
+ const searchResults = [];
126
+ if (response.data?.output && Array.isArray(response.data.output)) {
127
+ for (const outputItem of response.data.output) {
128
+ if (outputItem.type === 'message' &&
129
+ outputItem.role === 'assistant' &&
130
+ outputItem.content &&
131
+ Array.isArray(outputItem.content)) {
132
+ for (const contentItem of outputItem.content) {
133
+ if (contentItem.type === 'output_text' &&
134
+ contentItem.annotations &&
135
+ Array.isArray(contentItem.annotations)) {
136
+ for (const annotation of contentItem.annotations) {
137
+ if (annotation.type === 'url_citation') {
138
+ searchResults.push({
139
+ title: annotation.title || '',
140
+ url: annotation.url || '',
141
+ content: annotation.summary || '',
142
+ snippet: `${(annotation.summary || '').substring(0, 200)}...`,
143
+ source: annotation.site_name || 'volcengine',
144
+ publishedDate: annotation.publish_time || undefined
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ (0, logger_1.debug)(config, `火山引擎搜索成功,找到 ${searchResults.length} 条结果 (模型: ${actualModel})`, 'Search-Volcengine', 'details');
154
+ return {
155
+ success: true,
156
+ results: searchResults,
157
+ query,
158
+ engine: 'volcengine',
159
+ model: actualModel
160
+ };
161
+ }
162
+ catch (error) {
163
+ const errorMsg = `火山引擎搜索失败: ${error.message}`;
164
+ (0, logger_1.debug)(config, errorMsg, 'Search-Volcengine', 'error');
165
+ (0, search_rotation_1.markVolcengineModelFailure)(config, searchConfig, actualModel);
166
+ return {
167
+ success: false,
168
+ results: [],
169
+ query,
170
+ engine: 'volcengine',
171
+ model: actualModel,
172
+ error: errorMsg
173
+ };
174
+ }
175
+ }