@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.
- 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
package/lib/core/ai.js
CHANGED
|
@@ -1,524 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.AiSummaryCache = void 0;
|
|
40
|
-
|
|
41
|
-
exports.
|
|
42
|
-
exports.
|
|
43
|
-
exports.
|
|
44
|
-
exports.
|
|
45
|
-
exports.
|
|
46
|
-
|
|
47
|
-
exports
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const logger_1 = require("../utils/logger");
|
|
53
|
-
const search_1 = require("./search");
|
|
54
|
-
const config_1 = require("../config");
|
|
55
|
-
/**
|
|
56
|
-
* AI 摘要缓存管理器
|
|
57
|
-
*/
|
|
58
|
-
class AiSummaryCache {
|
|
59
|
-
cache = new Map();
|
|
60
|
-
ttl; // 缓存过期时间(毫秒)
|
|
61
|
-
maxSize; // 最大缓存条数
|
|
62
|
-
accessOrder = [];
|
|
63
|
-
constructor(ttl = 24 * 60 * 60 * 1000, maxSize = 1000) {
|
|
64
|
-
this.ttl = ttl;
|
|
65
|
-
this.maxSize = maxSize;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* 触发 LRU 淘汰,移除最久未访问的条目
|
|
69
|
-
*/
|
|
70
|
-
evictIfNeeded() {
|
|
71
|
-
// 如果缓存未达到最大限制,不需要淘汰
|
|
72
|
-
// 注意:这里用 > 而不是 >=,确保添加新项后不会超过限制
|
|
73
|
-
while (this.cache.size >= this.maxSize && this.accessOrder.length > 0) {
|
|
74
|
-
const oldestKey = this.accessOrder.shift();
|
|
75
|
-
if (oldestKey && this.cache.has(oldestKey)) {
|
|
76
|
-
this.cache.delete(oldestKey);
|
|
77
|
-
}
|
|
78
|
-
// 防止死循环,如果 accessOrder 为空但 cache 仍满,随机删除一个
|
|
79
|
-
if (this.accessOrder.length === 0 && this.cache.size >= this.maxSize) {
|
|
80
|
-
const randomKey = this.cache.keys().next().value;
|
|
81
|
-
if (randomKey) {
|
|
82
|
-
this.cache.delete(randomKey);
|
|
83
|
-
}
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* 更新访问顺序(LRU)
|
|
90
|
-
*/
|
|
91
|
-
updateAccessOrder(key) {
|
|
92
|
-
// 从当前位置移除(如果存在)
|
|
93
|
-
const index = this.accessOrder.indexOf(key);
|
|
94
|
-
if (index > -1) {
|
|
95
|
-
this.accessOrder.splice(index, 1);
|
|
96
|
-
}
|
|
97
|
-
// 添加到末尾(最近访问)
|
|
98
|
-
this.accessOrder.push(key);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* 从访问顺序中移除指定键
|
|
102
|
-
*/
|
|
103
|
-
removeAccessOrder(key) {
|
|
104
|
-
const index = this.accessOrder.indexOf(key);
|
|
105
|
-
if (index > -1) {
|
|
106
|
-
this.accessOrder.splice(index, 1);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* 生成缓存键(基于内容的哈希)
|
|
111
|
-
*/
|
|
112
|
-
generateKey(title, content) {
|
|
113
|
-
const hash = crypto
|
|
114
|
-
.createHash('sha256')
|
|
115
|
-
.update(`${title}|||${content}`)
|
|
116
|
-
.digest('hex');
|
|
117
|
-
return hash;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* 获取缓存
|
|
121
|
-
*/
|
|
122
|
-
get(title, content) {
|
|
123
|
-
const key = this.generateKey(title, content);
|
|
124
|
-
const entry = this.cache.get(key);
|
|
125
|
-
if (!entry)
|
|
126
|
-
return null;
|
|
127
|
-
// 检查是否过期
|
|
128
|
-
if (Date.now() - entry.timestamp > this.ttl) {
|
|
129
|
-
this.cache.delete(key);
|
|
130
|
-
this.removeAccessOrder(key);
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
// 更新访问顺序(LRU)
|
|
134
|
-
this.updateAccessOrder(key);
|
|
135
|
-
entry.lastAccess = Date.now();
|
|
136
|
-
return entry.summary;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* 设置缓存
|
|
140
|
-
*/
|
|
141
|
-
set(title, content, summary) {
|
|
142
|
-
const key = this.generateKey(title, content);
|
|
143
|
-
// 如果已存在,更新访问顺序并更新时间戳
|
|
144
|
-
if (this.cache.has(key)) {
|
|
145
|
-
this.updateAccessOrder(key);
|
|
146
|
-
const entry = this.cache.get(key);
|
|
147
|
-
entry.summary = summary;
|
|
148
|
-
entry.timestamp = Date.now();
|
|
149
|
-
entry.lastAccess = Date.now();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// 如果是新条目,先检查是否需要淘汰
|
|
153
|
-
this.evictIfNeeded();
|
|
154
|
-
this.cache.set(key, {
|
|
155
|
-
summary,
|
|
156
|
-
timestamp: Date.now(),
|
|
157
|
-
lastAccess: Date.now()
|
|
158
|
-
});
|
|
159
|
-
// 添加到访问顺序
|
|
160
|
-
this.accessOrder.push(key);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* 清除过期缓存
|
|
164
|
-
*/
|
|
165
|
-
cleanExpired() {
|
|
166
|
-
const now = Date.now();
|
|
167
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
168
|
-
if (now - entry.timestamp > this.ttl) {
|
|
169
|
-
this.cache.delete(key);
|
|
170
|
-
this.removeAccessOrder(key);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* 清空所有缓存
|
|
176
|
-
*/
|
|
177
|
-
clear() {
|
|
178
|
-
this.cache.clear();
|
|
179
|
-
this.accessOrder = [];
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* 获取缓存统计
|
|
183
|
-
*/
|
|
184
|
-
getStats() {
|
|
185
|
-
return {
|
|
186
|
-
size: this.cache.size,
|
|
187
|
-
keys: Array.from(this.cache.keys())
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
exports.AiSummaryCache = AiSummaryCache;
|
|
192
|
-
// 全局缓存实例
|
|
193
|
-
let globalCache = null;
|
|
194
|
-
/**
|
|
195
|
-
* 初始化 AI 摘要缓存
|
|
196
|
-
*/
|
|
197
|
-
function initAiCache(ttl, maxSize) {
|
|
198
|
-
if (!globalCache) {
|
|
199
|
-
// 使用配置中的 maxSize 或默认值 1000
|
|
200
|
-
const defaultMaxSize = 1000;
|
|
201
|
-
globalCache = new AiSummaryCache(ttl, maxSize || defaultMaxSize);
|
|
202
|
-
(0, logger_1.debug)({ debug: 'info' }, `AI 摘要缓存已初始化 (TTL: ${ttl || 24 * 60 * 60 * 1000}ms, MaxSize: ${maxSize || defaultMaxSize})`, 'AI-Cache', 'info');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* 清洗 HTML 内容为纯文本
|
|
207
|
-
*/
|
|
208
|
-
function cleanHtmlContent(contentHtml, maxLength) {
|
|
209
|
-
const $ = cheerio.load(contentHtml || '');
|
|
210
|
-
// 移除脚本、样式、图片等无关标签
|
|
211
|
-
$('script').remove();
|
|
212
|
-
$('style').remove();
|
|
213
|
-
$('img').remove();
|
|
214
|
-
$('video').remove();
|
|
215
|
-
let plainText = $.text().replace(/\s+/g, ' ').trim();
|
|
216
|
-
// 截断超长文本
|
|
217
|
-
if (plainText.length > maxLength) {
|
|
218
|
-
plainText = plainText.substring(0, maxLength) + '...';
|
|
219
|
-
}
|
|
220
|
-
return plainText;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* 构建代理配置
|
|
224
|
-
*/
|
|
225
|
-
function buildProxyConfig(config) {
|
|
226
|
-
const requestConfig = {};
|
|
227
|
-
if (config.net.proxyAgent?.enabled) {
|
|
228
|
-
const proxyUrl = `${config.net.proxyAgent.protocol}://${config.net.proxyAgent.host}:${config.net.proxyAgent.port}`;
|
|
229
|
-
requestConfig.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl);
|
|
230
|
-
requestConfig.proxy = false;
|
|
231
|
-
}
|
|
232
|
-
return requestConfig;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* 调用 AI API 生成摘要(带智能降级)
|
|
236
|
-
*/
|
|
237
|
-
async function callAiApi(config, prompt, context) {
|
|
238
|
-
// 最大重试次数
|
|
239
|
-
const maxRetries = 2;
|
|
240
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
241
|
-
try {
|
|
242
|
-
(0, logger_1.debug)(config, `AI 请求尝试 ${attempt + 1}/${maxRetries + 1}: ${context}`, 'AI', 'info');
|
|
243
|
-
const requestConfig = {
|
|
244
|
-
headers: {
|
|
245
|
-
'Authorization': `Bearer ${config.ai.apiKey}`,
|
|
246
|
-
'Content-Type': 'application/json'
|
|
247
|
-
},
|
|
248
|
-
timeout: config.ai.timeout,
|
|
249
|
-
...buildProxyConfig(config)
|
|
250
|
-
};
|
|
251
|
-
const response = await axios_1.default.post(`${config.ai.baseUrl.replace(/\/+$/, '')}/chat/completions`, {
|
|
252
|
-
model: config.ai.model,
|
|
253
|
-
messages: [
|
|
254
|
-
{ role: 'user', content: prompt }
|
|
255
|
-
],
|
|
256
|
-
temperature: 0.7
|
|
257
|
-
}, requestConfig);
|
|
258
|
-
const summary = response.data?.choices?.[0]?.message?.content?.trim();
|
|
259
|
-
if (!summary) {
|
|
260
|
-
throw new Error('AI 返回空结果');
|
|
261
|
-
}
|
|
262
|
-
(0, logger_1.debug)(config, `AI 摘要生成成功: ${summary.substring(0, 20)}...`, 'AI', 'details');
|
|
263
|
-
return {
|
|
264
|
-
success: true,
|
|
265
|
-
summary,
|
|
266
|
-
cached: false
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
const isLastAttempt = attempt === maxRetries;
|
|
271
|
-
(0, logger_1.debug)(config, `AI 请求失败 (尝试 ${attempt + 1}/${maxRetries + 1}): ${error.message}`, 'AI', isLastAttempt ? 'error' : 'info');
|
|
272
|
-
// 如果是最后一次尝试,返回降级结果
|
|
273
|
-
if (isLastAttempt) {
|
|
274
|
-
return {
|
|
275
|
-
success: false,
|
|
276
|
-
summary: '',
|
|
277
|
-
cached: false,
|
|
278
|
-
error: error.message
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
// 等待后重试(指数退避)
|
|
282
|
-
if (attempt < maxRetries) {
|
|
283
|
-
const waitTime = Math.pow(2, attempt) * 1000; // 1s, 2s
|
|
284
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
// 不应该到这里,但为了类型安全
|
|
289
|
-
return {
|
|
290
|
-
success: false,
|
|
291
|
-
summary: '',
|
|
292
|
-
cached: false,
|
|
293
|
-
error: '未知错误'
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* 生成单条 AI 摘要(带缓存和降级)
|
|
298
|
-
*/
|
|
299
|
-
async function getAiSummary(config, title, contentHtml) {
|
|
300
|
-
// AI 功能未启用
|
|
301
|
-
if (!config.ai.enabled || !config.ai.apiKey)
|
|
302
|
-
return '';
|
|
303
|
-
// 初始化缓存(如果还没初始化)
|
|
304
|
-
if (!globalCache) {
|
|
305
|
-
initAiCache(undefined, config.security?.maxCacheSize);
|
|
306
|
-
}
|
|
307
|
-
// 清洗内容
|
|
308
|
-
const plainText = cleanHtmlContent(contentHtml, config.ai.maxInputLength);
|
|
309
|
-
// 内容太少不总结
|
|
310
|
-
if (!plainText || plainText.length < 50)
|
|
311
|
-
return '';
|
|
312
|
-
// 检查缓存
|
|
313
|
-
const cachedSummary = globalCache.get(title, plainText);
|
|
314
|
-
if (cachedSummary) {
|
|
315
|
-
(0, logger_1.debug)(config, `使用缓存的 AI 摘要: ${title}`, 'AI-Cache', 'details');
|
|
316
|
-
return cachedSummary;
|
|
317
|
-
}
|
|
318
|
-
// 构建 Prompt
|
|
319
|
-
let prompt = config.ai.prompt
|
|
320
|
-
.replace('{{title}}', title || '')
|
|
321
|
-
.replace('{{content}}', plainText);
|
|
322
|
-
// 如果启用了联网搜索,进行搜索并增强 Prompt
|
|
323
|
-
if (config.search?.enabled) {
|
|
324
|
-
try {
|
|
325
|
-
// 将扁平化的配置转换为嵌套的 SearchConfig
|
|
326
|
-
const normalizedSearchConfig = (0, config_1.normalizeSearchConfig)(config.search);
|
|
327
|
-
// 生成搜索查询(使用标题作为查询)
|
|
328
|
-
const searchQuery = title || plainText.substring(0, 100);
|
|
329
|
-
(0, logger_1.debug)(config, `正在联网搜索: ${searchQuery}`, 'AI-Search', 'info');
|
|
330
|
-
// 执行搜索
|
|
331
|
-
const searchResults = await (0, search_1.webSearch)(config, searchQuery, normalizedSearchConfig);
|
|
332
|
-
// 如果搜索成功,将搜索结果添加到 Prompt 中
|
|
333
|
-
if (searchResults.success && searchResults.results.length > 0) {
|
|
334
|
-
(0, logger_1.debug)(config, `联网搜索成功,找到 ${searchResults.results.length} 条结果`, 'AI-Search', 'details');
|
|
335
|
-
prompt = (0, search_1.buildPromptWithSearchContext)(prompt, searchResults, searchQuery);
|
|
336
|
-
}
|
|
337
|
-
else if (searchResults.error) {
|
|
338
|
-
(0, logger_1.debug)(config, `联网搜索失败: ${searchResults.error}`, 'AI-Search', 'info');
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
(0, logger_1.debug)(config, `联网搜索异常: ${error.message}`, 'AI-Search', 'error');
|
|
343
|
-
// 搜索失败时不影响摘要生成,继续使用原始 Prompt
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// 调用 AI API
|
|
347
|
-
const result = await callAiApi(config, prompt, `单条摘要: ${title}`);
|
|
348
|
-
// 缓存成功的结果
|
|
349
|
-
if (result.success && result.summary) {
|
|
350
|
-
globalCache.set(title, plainText, result.summary);
|
|
351
|
-
}
|
|
352
|
-
return result.summary;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* 批量生成 AI 摘要(多条更新合并为一条)
|
|
356
|
-
*/
|
|
357
|
-
async function getBatchAiSummary(config, items) {
|
|
358
|
-
// AI 功能未启用或项目数量为0
|
|
359
|
-
if (!config.ai.enabled || !config.ai.apiKey || items.length === 0)
|
|
360
|
-
return '';
|
|
361
|
-
// 只有一条时使用单条摘要
|
|
362
|
-
if (items.length === 1) {
|
|
363
|
-
return getAiSummary(config, items[0].title, items[0].content);
|
|
364
|
-
}
|
|
365
|
-
(0, logger_1.debug)(config, `批量生成 AI 摘要: ${items.length} 条内容`, 'AI-Batch', 'info');
|
|
366
|
-
try {
|
|
367
|
-
// 清洗所有内容
|
|
368
|
-
const cleanedItems = items
|
|
369
|
-
.map(item => ({
|
|
370
|
-
title: item.title,
|
|
371
|
-
content: cleanHtmlContent(item.content, config.ai.maxInputLength / items.length)
|
|
372
|
-
}))
|
|
373
|
-
.filter(item => item.content.length >= 50); // 过滤太短的内容
|
|
374
|
-
if (cleanedItems.length === 0) {
|
|
375
|
-
(0, logger_1.debug)(config, '所有内容都太短,无法生成批量摘要', 'AI-Batch', 'info');
|
|
376
|
-
return '';
|
|
377
|
-
}
|
|
378
|
-
// 构建批量 Prompt
|
|
379
|
-
let prompt = `请简要总结以下 ${cleanedItems.length} 条新闻/文章的核心内容,要求:
|
|
380
|
-
1. 语言简洁流畅,每条总结不超过30字
|
|
381
|
-
2. 按顺序总结,使用数字编号
|
|
382
|
-
3. 突出重点信息
|
|
383
|
-
|
|
384
|
-
${cleanedItems.map((item, index) => `
|
|
385
|
-
${index + 1}. 标题:${item.title}
|
|
386
|
-
内容:${item.content}
|
|
387
|
-
`).join('\n')}
|
|
388
|
-
|
|
389
|
-
总结:`;
|
|
390
|
-
// 如果启用了联网搜索,对第一个或最重要的标题进行搜索
|
|
391
|
-
if (config.search?.enabled && cleanedItems.length > 0) {
|
|
392
|
-
try {
|
|
393
|
-
// 将扁平化的配置转换为嵌套的 SearchConfig
|
|
394
|
-
const normalizedSearchConfig = (0, config_1.normalizeSearchConfig)(config.search);
|
|
395
|
-
// 使用第一条内容的标题作为搜索查询
|
|
396
|
-
const searchQuery = cleanedItems[0].title;
|
|
397
|
-
(0, logger_1.debug)(config, `批量摘要 - 正在联网搜索: ${searchQuery}`, 'AI-Search', 'info');
|
|
398
|
-
// 执行搜索
|
|
399
|
-
const searchResults = await (0, search_1.webSearch)(config, searchQuery, normalizedSearchConfig);
|
|
400
|
-
// 如果搜索成功,将搜索结果添加到 Prompt 中
|
|
401
|
-
if (searchResults.success && searchResults.results.length > 0) {
|
|
402
|
-
(0, logger_1.debug)(config, `批量摘要 - 联网搜索成功,找到 ${searchResults.results.length} 条结果`, 'AI-Search', 'details');
|
|
403
|
-
prompt = (0, search_1.buildPromptWithSearchContext)(prompt, searchResults, searchQuery);
|
|
404
|
-
}
|
|
405
|
-
else if (searchResults.error) {
|
|
406
|
-
(0, logger_1.debug)(config, `批量摘要 - 联网搜索失败: ${searchResults.error}`, 'AI-Search', 'info');
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
catch (error) {
|
|
410
|
-
(0, logger_1.debug)(config, `批量摘要 - 联网搜索异常: ${error.message}`, 'AI-Search', 'error');
|
|
411
|
-
// 搜索失败时不影响摘要生成,继续使用原始 Prompt
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// 调用 AI API
|
|
415
|
-
const result = await callAiApi(config, prompt, `批量摘要: ${cleanedItems.length}条`);
|
|
416
|
-
if (result.success && result.summary) {
|
|
417
|
-
(0, logger_1.debug)(config, `批量摘要生成成功: ${result.summary.substring(0, 50)}...`, 'AI-Batch', 'details');
|
|
418
|
-
}
|
|
419
|
-
return result.summary;
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
(0, logger_1.debug)(config, `批量摘要生成失败: ${error.message}`, 'AI-Batch', 'error');
|
|
423
|
-
return '';
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* 智能摘要:根据内容数量自动选择单条或批量摘要
|
|
428
|
-
*/
|
|
429
|
-
async function getSmartAiSummary(config, items) {
|
|
430
|
-
if (!config.ai.enabled || !config.ai.apiKey || items.length === 0) {
|
|
431
|
-
return '';
|
|
432
|
-
}
|
|
433
|
-
// 根据配置决定是否使用批量摘要
|
|
434
|
-
const threshold = 3; // 超过3条时使用批量摘要
|
|
435
|
-
if (items.length > threshold) {
|
|
436
|
-
return getBatchAiSummary(config, items);
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
// 少量内容时,生成单条摘要后合并
|
|
440
|
-
const summaries = await Promise.all(items.map(item => getAiSummary(config, item.title, item.content)));
|
|
441
|
-
return summaries.filter(s => s).join('\n\n');
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* 清除过期缓存
|
|
446
|
-
*/
|
|
447
|
-
function cleanExpiredCache() {
|
|
448
|
-
if (globalCache) {
|
|
449
|
-
globalCache.cleanExpired();
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* 清空所有缓存
|
|
454
|
-
*/
|
|
455
|
-
function clearAiCache() {
|
|
456
|
-
if (globalCache) {
|
|
457
|
-
globalCache.clear();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* 获取缓存统计信息
|
|
462
|
-
*/
|
|
463
|
-
function getAiCacheStats() {
|
|
464
|
-
if (globalCache) {
|
|
465
|
-
return globalCache.getStats();
|
|
466
|
-
}
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
// ============ 导出原有的 AI 选择器生成功能 ============
|
|
470
|
-
/**
|
|
471
|
-
* AI 智能生成 CSS 选择器(保持原有功能)
|
|
472
|
-
*/
|
|
473
|
-
async function generateSelectorByAI(config, url, instruction, html) {
|
|
474
|
-
if (!config.ai.enabled || !config.ai.apiKey) {
|
|
475
|
-
throw new Error('需在配置中开启 AI 功能并填写 API Key');
|
|
476
|
-
}
|
|
477
|
-
// 预处理 HTML
|
|
478
|
-
const $ = cheerio.load(html);
|
|
479
|
-
$('script, style, svg, path, link, meta, noscript').remove();
|
|
480
|
-
$('*').contents().each((_, e) => {
|
|
481
|
-
if (e.type === 'comment')
|
|
482
|
-
$(e).remove();
|
|
483
|
-
});
|
|
484
|
-
// 限制长度节省 token
|
|
485
|
-
let cleanHtml = $('body').html()?.replace(/\s+/g, ' ').trim().substring(0, 15000) || '';
|
|
486
|
-
const prompt = `
|
|
487
|
-
作为一名爬虫专家,请根据提供的 HTML 代码片段,为一个网页监控工具生成一个 CSS Selector。
|
|
488
|
-
|
|
489
|
-
目标网页:${url}
|
|
490
|
-
用户需求:${instruction}
|
|
491
|
-
|
|
492
|
-
要求:
|
|
493
|
-
1. 只返回 CSS Selector 字符串,不要包含任何解释、Markdown 标记或代码块符号。
|
|
494
|
-
2. Selector 必须尽可能精确,通常用于提取列表中的一项或多项。
|
|
495
|
-
3. 如果是列表,请确保 Selector 能选中列表项的容器。
|
|
496
|
-
|
|
497
|
-
HTML片段:
|
|
498
|
-
${cleanHtml}
|
|
499
|
-
`;
|
|
500
|
-
try {
|
|
501
|
-
(0, logger_1.debug)(config, `正在请求 AI 生成选择器: ${instruction}`, 'AI-Selector', 'info');
|
|
502
|
-
const requestConfig = {
|
|
503
|
-
headers: {
|
|
504
|
-
'Authorization': `Bearer ${config.ai.apiKey}`,
|
|
505
|
-
'Content-Type': 'application/json'
|
|
506
|
-
},
|
|
507
|
-
timeout: 60000,
|
|
508
|
-
...buildProxyConfig(config)
|
|
509
|
-
};
|
|
510
|
-
const response = await axios_1.default.post(`${config.ai.baseUrl.replace(/\/+$/, '')}/chat/completions`, {
|
|
511
|
-
model: config.ai.model,
|
|
512
|
-
messages: [{ role: 'user', content: prompt }],
|
|
513
|
-
temperature: 0.1
|
|
514
|
-
}, requestConfig);
|
|
515
|
-
let selector = response.data?.choices?.[0]?.message?.content?.trim();
|
|
516
|
-
selector = selector?.replace(/`/g, '')?.replace(/^css/i, '')?.trim();
|
|
517
|
-
(0, logger_1.debug)(config, `AI 生成的选择器: ${selector}`, 'AI-Selector', 'info');
|
|
518
|
-
return selector || '';
|
|
519
|
-
}
|
|
520
|
-
catch (error) {
|
|
521
|
-
(0, logger_1.debug)(config, `AI 生成选择器失败: ${error.message}`, 'AI-Selector', 'error');
|
|
522
|
-
throw error;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
3
|
+
exports.getSmartAiSummary = exports.getBatchAiSummary = exports.getAiSummary = exports.generateSelectorByAI = exports.initAiCache = exports.getAiCacheStats = exports.clearAiCache = exports.cleanExpiredCache = exports.AiSummaryCache = void 0;
|
|
4
|
+
var ai_cache_1 = require("./ai-cache");
|
|
5
|
+
Object.defineProperty(exports, "AiSummaryCache", { enumerable: true, get: function () { return ai_cache_1.AiSummaryCache; } });
|
|
6
|
+
Object.defineProperty(exports, "cleanExpiredCache", { enumerable: true, get: function () { return ai_cache_1.cleanExpiredCache; } });
|
|
7
|
+
Object.defineProperty(exports, "clearAiCache", { enumerable: true, get: function () { return ai_cache_1.clearAiCache; } });
|
|
8
|
+
Object.defineProperty(exports, "getAiCacheStats", { enumerable: true, get: function () { return ai_cache_1.getAiCacheStats; } });
|
|
9
|
+
Object.defineProperty(exports, "initAiCache", { enumerable: true, get: function () { return ai_cache_1.initAiCache; } });
|
|
10
|
+
var ai_selector_1 = require("./ai-selector");
|
|
11
|
+
Object.defineProperty(exports, "generateSelectorByAI", { enumerable: true, get: function () { return ai_selector_1.generateSelectorByAI; } });
|
|
12
|
+
var ai_summary_1 = require("./ai-summary");
|
|
13
|
+
Object.defineProperty(exports, "getAiSummary", { enumerable: true, get: function () { return ai_summary_1.getAiSummary; } });
|
|
14
|
+
Object.defineProperty(exports, "getBatchAiSummary", { enumerable: true, get: function () { return ai_summary_1.getBatchAiSummary; } });
|
|
15
|
+
Object.defineProperty(exports, "getSmartAiSummary", { enumerable: true, get: function () { return ai_summary_1.getSmartAiSummary; } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Config, rssArg } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* 将命令选项解析为订阅参数对象。
|
|
4
|
+
*
|
|
5
|
+
* @param options - 命令选项
|
|
6
|
+
* @param config - 插件配置
|
|
7
|
+
* @returns 解析后的订阅参数
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatArg(options: any, config: Config): rssArg;
|
|
10
|
+
/**
|
|
11
|
+
* 合并全局配置与订阅级参数。
|
|
12
|
+
*
|
|
13
|
+
* @param arg - 订阅级参数
|
|
14
|
+
* @param config - 插件配置
|
|
15
|
+
* @returns 合并后的运行时参数
|
|
16
|
+
*/
|
|
17
|
+
export declare function mixinArg(arg: any, config: Config): rssArg;
|