@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.
- package/README.md +92 -37
- package/lib/commands/error-handler.js +13 -1
- package/lib/commands/index.d.ts +3 -0
- package/lib/commands/index.js +7 -1
- package/lib/commands/runtime.d.ts +17 -0
- package/lib/commands/runtime.js +27 -0
- package/lib/commands/subscription-create.d.ts +23 -0
- package/lib/commands/subscription-create.js +145 -0
- package/lib/commands/web-monitor.d.ts +15 -0
- package/lib/commands/web-monitor.js +222 -0
- package/lib/config.js +7 -1
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +46 -83
- package/lib/core/ai-cache.d.ts +27 -0
- package/lib/core/ai-cache.js +169 -0
- package/lib/core/ai-client.d.ts +12 -0
- package/lib/core/ai-client.js +65 -0
- package/lib/core/ai-selector.d.ts +2 -0
- package/lib/core/ai-selector.js +80 -0
- package/lib/core/ai-summary.d.ts +10 -0
- package/lib/core/ai-summary.js +73 -0
- package/lib/core/ai-utils.d.ts +10 -0
- package/lib/core/ai-utils.js +104 -0
- package/lib/core/ai.d.ts +3 -91
- package/lib/core/ai.js +13 -522
- package/lib/core/feeder-arg.d.ts +17 -0
- package/lib/core/feeder-arg.js +234 -0
- package/lib/core/feeder-runtime.d.ts +96 -0
- package/lib/core/feeder-runtime.js +233 -0
- package/lib/core/feeder.d.ts +3 -5
- package/lib/core/feeder.js +61 -358
- package/lib/core/item-processor-runtime.d.ts +46 -0
- package/lib/core/item-processor-runtime.js +215 -0
- package/lib/core/item-processor-template.d.ts +16 -0
- package/lib/core/item-processor-template.js +158 -0
- package/lib/core/item-processor.d.ts +1 -15
- package/lib/core/item-processor.js +44 -319
- package/lib/core/notification-queue-retry.d.ts +25 -0
- package/lib/core/notification-queue-retry.js +78 -0
- package/lib/core/notification-queue-sender.d.ts +20 -0
- package/lib/core/notification-queue-sender.js +118 -0
- package/lib/core/notification-queue-store.d.ts +19 -0
- package/lib/core/notification-queue-store.js +137 -0
- package/lib/core/notification-queue-types.d.ts +49 -0
- package/lib/core/notification-queue-types.js +2 -0
- package/lib/core/notification-queue.d.ts +11 -72
- package/lib/core/notification-queue.js +81 -258
- package/lib/core/search-format.d.ts +3 -0
- package/lib/core/search-format.js +36 -0
- package/lib/core/search-providers.d.ts +13 -0
- package/lib/core/search-providers.js +175 -0
- package/lib/core/search-rotation.d.ts +4 -0
- package/lib/core/search-rotation.js +55 -0
- package/lib/core/search-service.d.ts +3 -0
- package/lib/core/search-service.js +100 -0
- package/lib/core/search-types.d.ts +39 -0
- package/lib/core/search-types.js +2 -0
- package/lib/core/search.d.ts +4 -101
- package/lib/core/search.js +10 -508
- package/lib/index.js +27 -381
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +27 -6
- package/lib/utils/legacy-config.d.ts +12 -0
- package/lib/utils/legacy-config.js +56 -0
- package/lib/utils/logger.js +50 -29
- package/lib/utils/proxy.d.ts +3 -0
- package/lib/utils/proxy.js +14 -0
- package/lib/utils/structured-logger.d.ts +7 -3
- package/lib/utils/structured-logger.js +26 -19
- 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
|
-
|
|
52
|
-
batchSize
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
145
|
+
queueErrorAction: classification.action,
|
|
194
146
|
});
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
queueErrorAction: classification.action,
|
|
210
160
|
});
|
|
211
161
|
return;
|
|
212
162
|
}
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
207
|
+
return resetCount;
|
|
382
208
|
}
|
|
383
209
|
/**
|
|
384
210
|
* 清理旧的成功任务
|
|
385
211
|
*/
|
|
386
|
-
async cleanupSuccessTasks(olderThanHours
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
219
|
+
return cleanupCount;
|
|
397
220
|
}
|
|
398
221
|
}
|
|
399
222
|
exports.NotificationQueueManager = NotificationQueueManager;
|
|
@@ -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
|
+
}
|