@anyul/koishi-plugin-rss 5.2.2 → 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 -4
- package/lib/commands/index.d.ts +20 -1
- package/lib/commands/index.js +394 -2
- 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/subscription-edit.d.ts +7 -0
- package/lib/commands/subscription-edit.js +177 -0
- package/lib/commands/subscription-management.d.ts +12 -0
- package/lib/commands/subscription-management.js +176 -0
- package/lib/commands/utils.d.ts +13 -1
- package/lib/commands/utils.js +43 -2
- package/lib/commands/web-monitor.d.ts +15 -0
- package/lib/commands/web-monitor.js +222 -0
- package/lib/config.js +25 -0
- 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 -77
- package/lib/core/ai.js +13 -455
- 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 +4 -6
- package/lib/core/feeder.js +120 -304
- 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 -10
- package/lib/core/item-processor.js +48 -393
- 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 +13 -72
- package/lib/core/notification-queue.js +132 -262
- package/lib/core/parser.js +12 -0
- package/lib/core/renderer.d.ts +15 -0
- package/lib/core/renderer.js +91 -23
- 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 +50 -1160
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +51 -6
- package/lib/utils/common.js +52 -3
- package/lib/utils/error-handler.d.ts +8 -0
- package/lib/utils/error-handler.js +27 -0
- package/lib/utils/error-tracker.js +24 -8
- package/lib/utils/fetcher.js +68 -9
- package/lib/utils/legacy-config.d.ts +12 -0
- package/lib/utils/legacy-config.js +56 -0
- package/lib/utils/logger.d.ts +4 -2
- package/lib/utils/logger.js +193 -34
- package/lib/utils/media.js +3 -6
- package/lib/utils/proxy.d.ts +3 -0
- package/lib/utils/proxy.js +14 -0
- package/lib/utils/sanitizer.d.ts +58 -0
- package/lib/utils/sanitizer.js +227 -0
- package/lib/utils/security.d.ts +75 -0
- package/lib/utils/security.js +312 -0
- package/lib/utils/structured-logger.d.ts +7 -3
- package/lib/utils/structured-logger.js +29 -39
- package/package.json +2 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerWebMonitorCommands = registerWebMonitorCommands;
|
|
4
|
+
const common_1 = require("../utils/common");
|
|
5
|
+
const error_handler_1 = require("../utils/error-handler");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
function registerWebMonitorCommands(deps) {
|
|
8
|
+
registerHtmlMonitorCommand(deps);
|
|
9
|
+
registerAskCommand(deps);
|
|
10
|
+
registerWatchCommand(deps);
|
|
11
|
+
}
|
|
12
|
+
function registerHtmlMonitorCommand(deps) {
|
|
13
|
+
deps.ctx.guild()
|
|
14
|
+
.command('rssowl.html <url:string>', '监控网页变化 (CSS Selector)')
|
|
15
|
+
.alias('rsso.html')
|
|
16
|
+
.usage(`
|
|
17
|
+
HTML 网页监控功能,使用 CSS 选择器提取内容
|
|
18
|
+
用法:
|
|
19
|
+
rsso.html https://example.com -s ".item" - 监控网页变化
|
|
20
|
+
rsso.html https://example.com -s ".item" -T - 测试选择器
|
|
21
|
+
rsso.html https://example.com -s ".item" -t "我的订阅" - 自定义标题
|
|
22
|
+
rsso.html https://example.com -s ".item" -P - SPA 动态页面
|
|
23
|
+
rsso.html https://example.com -s ".item" -w 5000 - 渲染后等待5秒
|
|
24
|
+
|
|
25
|
+
示例:
|
|
26
|
+
rsso.html https://www.zhihu.com/billboard -s ".BillBoard-item:first-child"
|
|
27
|
+
rsso.html https://news.ycombinator.com -s ".titleline > a"
|
|
28
|
+
`)
|
|
29
|
+
.option('selector', '-s <选择器> CSS 选择器 (必填)')
|
|
30
|
+
.option('title', '-t <标题> 自定义订阅标题')
|
|
31
|
+
.option('template', '-i <模板> 消息模板 (推荐 content)')
|
|
32
|
+
.option('text', '--text 只提取纯文本')
|
|
33
|
+
.option('puppeteer', '-P 使用 Puppeteer 渲染 (适用于SPA)')
|
|
34
|
+
.option('wait', '-w <毫秒> 渲染后等待时间')
|
|
35
|
+
.option('waitSelector', '-W <选择器> 等待特定元素出现')
|
|
36
|
+
.option('test', '-T 测试抓取结果 (不创建订阅)')
|
|
37
|
+
.example('rsso.html https://news.ycombinator.com -s ".titleline > a"')
|
|
38
|
+
.action(async ({ session, options }, url) => {
|
|
39
|
+
if (!url)
|
|
40
|
+
return '请输入 URL';
|
|
41
|
+
if (!options.selector)
|
|
42
|
+
return '请指定 CSS 选择器 (-s)';
|
|
43
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.html', options.test ? 'test' : 'create');
|
|
44
|
+
const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
|
|
45
|
+
const botSelfId = session.bot?.selfId;
|
|
46
|
+
const normalizedUrl = (0, common_1.ensureUrlProtocol)(url);
|
|
47
|
+
const rawArg = buildHtmlMonitorArg(options);
|
|
48
|
+
const arg = deps.mixinArg(rawArg);
|
|
49
|
+
try {
|
|
50
|
+
if (options.test) {
|
|
51
|
+
const items = await deps.getRssData(normalizedUrl, arg);
|
|
52
|
+
if (!items.length)
|
|
53
|
+
return '未找到符合选择器的元素';
|
|
54
|
+
return buildItemsPreview(items, `找到 ${items.length} 个元素:`, true);
|
|
55
|
+
}
|
|
56
|
+
const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
|
|
57
|
+
if (rssList.find(item => item.url === normalizedUrl))
|
|
58
|
+
return '该订阅已存在';
|
|
59
|
+
const htmlItems = await deps.getRssData(normalizedUrl, arg);
|
|
60
|
+
if (!htmlItems.length)
|
|
61
|
+
return '未找到符合选择器的元素,无法创建订阅';
|
|
62
|
+
const title = options.title || htmlItems[0]?.rss?.channel?.title || `HTML监控: ${normalizedUrl}`;
|
|
63
|
+
const rssItem = {
|
|
64
|
+
url: normalizedUrl,
|
|
65
|
+
platform,
|
|
66
|
+
guildId,
|
|
67
|
+
author: botSelfId,
|
|
68
|
+
rssId: title,
|
|
69
|
+
arg: rawArg,
|
|
70
|
+
title,
|
|
71
|
+
lastPubDate: new Date(),
|
|
72
|
+
lastContent: [],
|
|
73
|
+
followers: [],
|
|
74
|
+
};
|
|
75
|
+
if (deps.config.basic.urlDeduplication && rssList.find(item => item.rssId === rssItem.rssId)) {
|
|
76
|
+
return `订阅已存在: ${rssItem.rssId}`;
|
|
77
|
+
}
|
|
78
|
+
await deps.ctx.database.create('rssOwl', rssItem);
|
|
79
|
+
if (deps.config.basic.firstLoad && arg.firstLoad !== false && htmlItems.length > 0) {
|
|
80
|
+
await broadcastInitialItems(deps, `${platform}:${guildId}`, htmlItems, rssItem);
|
|
81
|
+
}
|
|
82
|
+
return `订阅成功: ${title}\n提示: HTML监控基于内容变化检测,请确保选择器稳定`;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
deps.debug(error, 'html error', 'error', logContext);
|
|
86
|
+
return `抓取失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, 'HTML监控')}`;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function registerAskCommand(deps) {
|
|
91
|
+
deps.ctx.guild()
|
|
92
|
+
.command('rssowl.ask <url:string> <instruction:text>', 'AI 智能订阅网页')
|
|
93
|
+
.alias('rsso.ask')
|
|
94
|
+
.usage(`AI 智能订阅功能,自动生成 CSS 选择器
|
|
95
|
+
|
|
96
|
+
前置要求:
|
|
97
|
+
- 需要配置 AI 功能 (config.ai.enabled = true)
|
|
98
|
+
- 需要配置 API Key (config.ai.apiKey)
|
|
99
|
+
|
|
100
|
+
用法:
|
|
101
|
+
rsso.ask https://news.ycombinator.com "监控首页的前5条新闻标题"
|
|
102
|
+
|
|
103
|
+
示例:
|
|
104
|
+
rsso.ask https://www.zhihu.com/billboard "获取热榜第一条"
|
|
105
|
+
rsso.ask https://example.com "提取所有文章标题" -T
|
|
106
|
+
`)
|
|
107
|
+
.option('test', '-T 测试模式 (只分析不订阅)')
|
|
108
|
+
.example('rsso.ask https://news.ycombinator.com "监控首页的前5条新闻标题"')
|
|
109
|
+
.action(async ({ session, options }, url, instruction) => {
|
|
110
|
+
if (!url)
|
|
111
|
+
return '请输入网址';
|
|
112
|
+
if (!instruction)
|
|
113
|
+
return '请描述你的需求';
|
|
114
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.ask', options.test ? 'test' : 'analyze');
|
|
115
|
+
const normalizedUrl = (0, common_1.ensureUrlProtocol)(url);
|
|
116
|
+
try {
|
|
117
|
+
const html = await deps.fetchUrl(normalizedUrl);
|
|
118
|
+
const selector = await deps.generateSelectorByAI(normalizedUrl, instruction, html);
|
|
119
|
+
if (options.test) {
|
|
120
|
+
const items = await deps.getRssData(normalizedUrl, {
|
|
121
|
+
type: 'html',
|
|
122
|
+
selector,
|
|
123
|
+
template: 'content',
|
|
124
|
+
});
|
|
125
|
+
if (!items.length)
|
|
126
|
+
return `选择器未匹配到任何元素: ${selector}`;
|
|
127
|
+
return `AI 生成的选择器: ${selector}\n\n匹配到 ${items.length} 个元素:\n${items.slice(0, 2).map((item) => normalizePreviewText(item?.title) || '无标题').join('\n')}`;
|
|
128
|
+
}
|
|
129
|
+
return `AI 生成的选择器: ${selector}\n请使用 rsso.html ${normalizedUrl} -s "${selector}" 完成订阅`;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
deps.debug(error, 'ask error', 'error', logContext);
|
|
133
|
+
return `AI 分析失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, 'AI生成选择器')}`;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function registerWatchCommand(deps) {
|
|
138
|
+
deps.ctx.guild()
|
|
139
|
+
.command('rssowl.watch <url:string> [keyword:text]', '简单网页监控')
|
|
140
|
+
.alias('rsso.watch')
|
|
141
|
+
.usage(`
|
|
142
|
+
简单网页监控,支持关键词或整页监控。
|
|
143
|
+
用法:
|
|
144
|
+
rsso.watch https://example.com - 监控整页变化
|
|
145
|
+
rsso.watch https://example.com "缺货" - 监控包含关键词的内容
|
|
146
|
+
rsso.watch https://example.com "缺货" -P - SPA 动态页面
|
|
147
|
+
rsso.watch https://example.com "缺货" -T - 测试模式 (只预览不订阅)
|
|
148
|
+
`)
|
|
149
|
+
.option('puppeteer', '-P 使用 Puppeteer 渲染')
|
|
150
|
+
.option('test', '-T 测试模式 (只预览不订阅)')
|
|
151
|
+
.example('rsso.watch https://example.com "缺货"')
|
|
152
|
+
.action(async ({ session, options }, url, keyword) => {
|
|
153
|
+
if (!url)
|
|
154
|
+
return '请输入 URL';
|
|
155
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.watch', options.test ? 'test' : 'preview');
|
|
156
|
+
const normalizedUrl = (0, common_1.ensureUrlProtocol)(url);
|
|
157
|
+
const arg = deps.mixinArg(buildWatchArg(keyword, options));
|
|
158
|
+
try {
|
|
159
|
+
if (options.test) {
|
|
160
|
+
const items = await deps.getRssData(normalizedUrl, arg);
|
|
161
|
+
if (!items.length)
|
|
162
|
+
return '未找到内容';
|
|
163
|
+
return buildItemsPreview(items, `找到 ${items.length} 条内容:`, false);
|
|
164
|
+
}
|
|
165
|
+
return '请使用 rsso 命令完成订阅,或使用 -T 测试';
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
deps.debug(error, 'watch error', 'error', logContext);
|
|
169
|
+
return `监控失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, '网页监控')}`;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function buildHtmlMonitorArg(options) {
|
|
174
|
+
return {
|
|
175
|
+
type: 'html',
|
|
176
|
+
selector: options.selector,
|
|
177
|
+
template: options.template || 'content',
|
|
178
|
+
textOnly: Boolean(options.text),
|
|
179
|
+
mode: options.puppeteer ? 'puppeteer' : 'static',
|
|
180
|
+
waitFor: options.wait ? parseInt(options.wait, 10) : undefined,
|
|
181
|
+
waitSelector: options.waitSelector,
|
|
182
|
+
title: options.title,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function buildWatchArg(keyword, options) {
|
|
186
|
+
return {
|
|
187
|
+
type: 'html',
|
|
188
|
+
selector: keyword ? `*:contains("${keyword}")` : 'body',
|
|
189
|
+
textOnly: Boolean(keyword),
|
|
190
|
+
mode: options.puppeteer ? 'puppeteer' : 'static',
|
|
191
|
+
template: 'content',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function broadcastInitialItems(deps, target, items, rssItem) {
|
|
195
|
+
const maxItem = rssItem.arg?.forceLength || 1;
|
|
196
|
+
const mergedArg = deps.mixinArg(rssItem.arg || {});
|
|
197
|
+
const messageList = await Promise.all(items
|
|
198
|
+
.filter((_, index) => index < maxItem)
|
|
199
|
+
.map(async (item) => deps.parseRssItem(item, { ...rssItem, ...mergedArg }, rssItem.author)));
|
|
200
|
+
await deps.ctx.broadcast([target], messageList.join(''));
|
|
201
|
+
}
|
|
202
|
+
function buildItemsPreview(items, header, withContentLabel) {
|
|
203
|
+
const preview = items.slice(0, 3).map((item) => {
|
|
204
|
+
const title = normalizePreviewText(item?.title) || '无标题';
|
|
205
|
+
const description = truncatePreviewText(normalizePreviewText(item?.description), 100);
|
|
206
|
+
const body = withContentLabel ? `内容: ${description}` : description;
|
|
207
|
+
return `标题: ${title}\n${body}`;
|
|
208
|
+
}).join('\n\n');
|
|
209
|
+
return `${header}\n\n${preview}`;
|
|
210
|
+
}
|
|
211
|
+
function normalizePreviewText(value) {
|
|
212
|
+
if (Array.isArray(value))
|
|
213
|
+
return value.join('');
|
|
214
|
+
if (value === undefined || value === null)
|
|
215
|
+
return '';
|
|
216
|
+
return String(value);
|
|
217
|
+
}
|
|
218
|
+
function truncatePreviewText(value, maxLength) {
|
|
219
|
+
if (value.length <= maxLength)
|
|
220
|
+
return value;
|
|
221
|
+
return `${value.substring(0, maxLength)}...`;
|
|
222
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -164,6 +164,22 @@ exports.Config = koishi_1.Schema.object({
|
|
|
164
164
|
enabled: koishi_1.Schema.boolean().description('启用消息缓存').default(true),
|
|
165
165
|
maxSize: koishi_1.Schema.number().description('最大缓存消息条数').default(100),
|
|
166
166
|
}).description('消息缓存设置'),
|
|
167
|
+
queue: koishi_1.Schema.object({
|
|
168
|
+
batchSize: koishi_1.Schema.number().min(1).max(50).description('每次处理发送队列的最大任务数').default(10),
|
|
169
|
+
maxRetries: koishi_1.Schema.number().min(0).max(20).description('临时错误的最大重试次数,超过后转为失败').default(5),
|
|
170
|
+
processInterval: koishi_1.Schema.number().min(5).max(3600).description('发送队列处理间隔(秒)').default(30),
|
|
171
|
+
cleanupHours: koishi_1.Schema.number().min(1).max(720).description('队列清理命令默认保留成功任务的小时数').default(24),
|
|
172
|
+
}).description('发送队列设置'),
|
|
173
|
+
security: koishi_1.Schema.object({
|
|
174
|
+
enabled: koishi_1.Schema.boolean().description('启用安全检查(建议开启)').default(false),
|
|
175
|
+
allowInternalAccess: koishi_1.Schema.boolean().description('允许访问内网 IP 地址(如本地部署的 RSSHub)').default(false),
|
|
176
|
+
whitelist: koishi_1.Schema.array(koishi_1.Schema.string()).description('URL 白名单域名').default([]),
|
|
177
|
+
blacklist: koishi_1.Schema.array(koishi_1.Schema.string()).description('URL 黑名单域名').default([]),
|
|
178
|
+
allowHttp: koishi_1.Schema.boolean().description('允许 HTTP 协议').default(true),
|
|
179
|
+
allowHttps: koishi_1.Schema.boolean().description('允许 HTTPS 协议').default(true),
|
|
180
|
+
sanitizeHtml: koishi_1.Schema.boolean().description('启用 RSS 原始 HTML 内容清理').default(true),
|
|
181
|
+
maxCacheSize: koishi_1.Schema.number().description('AI 摘要缓存最大条数').default(1000),
|
|
182
|
+
}).description('安全设置'),
|
|
167
183
|
// customUrlEnable:Schema.boolean().description('开发中:允许使用自定义规则对网页进行提取,用于对非RSS链接抓取').default(false).experimental(),
|
|
168
184
|
debug: koishi_1.Schema.union(["disable", "error", "info", "details"]).default("disable").description('调试级别'),
|
|
169
185
|
logging: koishi_1.Schema.object({
|
|
@@ -173,5 +189,14 @@ exports.Config = koishi_1.Schema.object({
|
|
|
173
189
|
includeModule: koishi_1.Schema.boolean().description('包含模块名').default(true),
|
|
174
190
|
includeContext: koishi_1.Schema.boolean().description('包含额外上下文信息').default(false),
|
|
175
191
|
contextFields: koishi_1.Schema.array(koishi_1.Schema.string()).description('要包含的上下文字段(如 guildId, platform 等)').default([]),
|
|
192
|
+
sanitizeLogs: koishi_1.Schema.boolean().description('自动脱敏日志中的敏感信息').default(true),
|
|
176
193
|
}).description('日志设置'),
|
|
194
|
+
errorTracking: koishi_1.Schema.object({
|
|
195
|
+
enabled: koishi_1.Schema.boolean().description('启用错误追踪').default(false),
|
|
196
|
+
dsn: koishi_1.Schema.string().role('secret').description('Sentry DSN').default(''),
|
|
197
|
+
environment: koishi_1.Schema.string().description('错误追踪环境').default('production'),
|
|
198
|
+
release: koishi_1.Schema.string().description('错误追踪版本号').default('5.2.3'),
|
|
199
|
+
tracesSampleRate: koishi_1.Schema.number().min(0).max(1).description('性能追踪采样率').default(0.1),
|
|
200
|
+
profilesSampleRate: koishi_1.Schema.number().min(0).max(1).description('性能分析采样率').default(0.1),
|
|
201
|
+
}).description('错误追踪设置'),
|
|
177
202
|
});
|
package/lib/constants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const usage = "\n<details>\n<summary>RSS-OWL \
|
|
1
|
+
export declare const usage = "\n<details>\n<summary>RSS-OWL \u547D\u4EE4\u5BFC\u822A\uFF08\u53D1\u9001 rsso \u67E5\u770B\u672C\u5E2E\u52A9\uFF09</summary>\n\n## \u65B0\u5EFA / \u6D4B\u8BD5\u8BA2\u9605:\n rsso <url> - \u521B\u5EFA RSS / Atom / JSON Feed \u8BA2\u9605\n rsso -T <url> - \u6D4B\u8BD5\u6293\u53D6\uFF0C\u4E0D\u5199\u5165\u8BA2\u9605\n rsso <url> -t <\u6807\u9898> - \u81EA\u5B9A\u4E49\u8BA2\u9605\u6807\u9898\n rsso <url> -i <\u6A21\u677F> - \u6307\u5B9A\u6D88\u606F\u6A21\u677F\n rsso <url> -a <key:value,...> - \u8986\u76D6\u8BA2\u9605\u53C2\u6570\n rsso <url> -d <HH:mm[/\u6570\u91CF]> - \u6BCF\u65E5\u5B9A\u65F6\u63A8\u9001\n rsso <url> --target <\u5E73\u53F0:\u9891\u9053> - \u8DE8\u7FA4 / \u8DE8\u9891\u9053\u8BA2\u9605\uFF08\u9AD8\u7EA7\u6743\u9650\uFF09\n rsso -q [\u7F16\u53F7] - \u67E5\u770B\u5FEB\u901F\u8BA2\u9605\u5217\u8868 / \u8BE6\u60C5\n\n## \u7BA1\u7406\u8BA2\u9605\uFF08\u4F7F\u7528\u5217\u8868\u5E8F\u53F7\uFF09:\n rsso.list [id] - \u67E5\u770B\u8BA2\u9605\u5217\u8868 / \u8BE6\u60C5\n rsso.remove <id> [--all] - \u5220\u9664\u8BA2\u9605 / \u5220\u9664\u5168\u90E8\n rsso.pull <id> - \u62C9\u53D6\u8BA2\u9605\u6700\u65B0\u5185\u5BB9\n rsso.follow <id> [--all] - \u5173\u6CE8\u8BA2\u9605\u66F4\u65B0 / \u5168\u5458\u63D0\u9192\n rsso.edit <id> [\u9009\u9879] - \u4FEE\u6539\u6807\u9898\u3001URL\u3001\u6A21\u677F\u3001\u9009\u62E9\u5668\u3001\u76EE\u6807\n rsso.cache - \u6D88\u606F\u7F13\u5B58\u7BA1\u7406\n rsso.queue - \u53D1\u9001\u961F\u5217\u7BA1\u7406\n\n## \u7F51\u9875\u76D1\u63A7\u76F8\u5173:\n rsso.html <url> -s <selector> - \u4F7F\u7528 CSS \u9009\u62E9\u5668\u76D1\u63A7\u7F51\u9875\n rsso.ask <url> <\u9700\u6C42> - AI \u751F\u6210\u9009\u62E9\u5668\u540E\u521B\u5EFA\u7F51\u9875\u8BA2\u9605\n rsso.watch <url> [\u5173\u952E\u8BCD] - \u7B80\u5355\u7F51\u9875 / \u5173\u952E\u8BCD\u76D1\u63A7\n\n## \u5E38\u7528\u6A21\u677F:\n content - \u7EAF\u6587\u5B57\u6B63\u6587\n default - \u9ED8\u8BA4\u56FE\u7247\u6A21\u677F\n custom - \u81EA\u5B9A\u4E49\u6A21\u677F\n only text - \u4EC5\u6587\u672C\n only image - \u4EC5\u56FE\u7247\n only media - \u56FE\u7247 + \u89C6\u9891\n link - \u8DDF\u968F\u6B63\u6587\u4E2D\u7684\u7B2C\u4E00\u4E2A\u94FE\u63A5\n\n## \u5E38\u7528\u793A\u4F8B:\n rsso https://example.com/rss\n rsso -T tg:woshadiao\n rsso https://example.com/rss -i content -t \"\u793A\u4F8B\u8BA2\u9605\"\n rsso.list\n rsso.edit 1 -t \"\u65B0\u6807\u9898\"\n rsso.html https://example.com -s \".news-item\"\n\n## \u517C\u5BB9\u63D0\u793A:\n \u65E7\u9009\u9879 -l / -r / -f / -p \u4ECD\u4F1A\u8FD4\u56DE\u8FC1\u79FB\u63D0\u793A\uFF0C\n \u5EFA\u8BAE\u6539\u7528 rsso.list / rsso.remove / rsso.follow / rsso.pull\u3002\n\n</details>\n";
|
|
2
2
|
export declare const quickList: {
|
|
3
3
|
prefix: string;
|
|
4
4
|
name: string;
|
package/lib/constants.js
CHANGED
|
@@ -3,89 +3,52 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.quickList = exports.usage = void 0;
|
|
4
4
|
exports.usage = `
|
|
5
5
|
<details>
|
|
6
|
-
<summary>RSS-OWL
|
|
7
|
-
|
|
8
|
-
##
|
|
9
|
-
rsso <url>
|
|
10
|
-
rsso -
|
|
11
|
-
rsso -
|
|
12
|
-
rsso
|
|
13
|
-
rsso
|
|
14
|
-
rsso
|
|
15
|
-
rsso
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
rsso
|
|
46
|
-
rsso
|
|
47
|
-
rsso
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<summary>网页监控 (rsso.html) - 监控任意网页元素变化</summary>
|
|
53
|
-
|
|
54
|
-
使用 CSS 选择器监控网页元素变化,支持静态网页和 SPA 动态页面。
|
|
55
|
-
|
|
56
|
-
## 基本用法:
|
|
57
|
-
rsso.html <url> -s <selector> - 监控符合选择器的元素
|
|
58
|
-
|
|
59
|
-
## 常用选项:
|
|
60
|
-
-s, --selector <选择器> CSS 选择器 (必填),例如: .news-item、#price、div.list > li
|
|
61
|
-
-t, --title <标题> 自定义订阅标题
|
|
62
|
-
-i, --template <模板> 消息模板 (推荐 content)
|
|
63
|
-
--text 只提取纯文本 (默认提取 HTML)
|
|
64
|
-
-T, --test 测试模式,查看抓取预览
|
|
65
|
-
|
|
66
|
-
## Puppeteer 动态渲染 (解决 SPA/JS 动态内容):
|
|
67
|
-
-P, --puppeteer 使用 Puppeteer 渲染页面 (需要安装 koishi-plugin-puppeteer)
|
|
68
|
-
-w, --wait <毫秒> 渲染后等待时间
|
|
69
|
-
-W, --waitSelector <选择器> 等待特定元素出现
|
|
70
|
-
|
|
71
|
-
</details>
|
|
72
|
-
|
|
73
|
-
<details>
|
|
74
|
-
<summary>AI 摘要 (ai) - 智能生成内容摘要</summary>
|
|
75
|
-
|
|
76
|
-
使用 OpenAI 兼容 API 为订阅内容生成 AI 摘要。
|
|
77
|
-
|
|
78
|
-
## 启用方法:
|
|
79
|
-
1. 在插件配置中开启 AI 功能
|
|
80
|
-
2. 填写 API Base URL、API Key 和模型名称
|
|
81
|
-
|
|
82
|
-
## 配置项:
|
|
83
|
-
- placement 摘要位置: top (顶部) / bottom (底部)
|
|
84
|
-
- separator 分割线样式
|
|
85
|
-
- prompt 提示词模板 ({{title}} 标题, {{content}} 内容)
|
|
86
|
-
- maxInputLength 最大输入长度 (默认 2000 字)
|
|
87
|
-
- timeout 请求超时 (默认 30000 毫秒)
|
|
88
|
-
|
|
6
|
+
<summary>RSS-OWL 命令导航(发送 rsso 查看本帮助)</summary>
|
|
7
|
+
|
|
8
|
+
## 新建 / 测试订阅:
|
|
9
|
+
rsso <url> - 创建 RSS / Atom / JSON Feed 订阅
|
|
10
|
+
rsso -T <url> - 测试抓取,不写入订阅
|
|
11
|
+
rsso <url> -t <标题> - 自定义订阅标题
|
|
12
|
+
rsso <url> -i <模板> - 指定消息模板
|
|
13
|
+
rsso <url> -a <key:value,...> - 覆盖订阅参数
|
|
14
|
+
rsso <url> -d <HH:mm[/数量]> - 每日定时推送
|
|
15
|
+
rsso <url> --target <平台:频道> - 跨群 / 跨频道订阅(高级权限)
|
|
16
|
+
rsso -q [编号] - 查看快速订阅列表 / 详情
|
|
17
|
+
|
|
18
|
+
## 管理订阅(使用列表序号):
|
|
19
|
+
rsso.list [id] - 查看订阅列表 / 详情
|
|
20
|
+
rsso.remove <id> [--all] - 删除订阅 / 删除全部
|
|
21
|
+
rsso.pull <id> - 拉取订阅最新内容
|
|
22
|
+
rsso.follow <id> [--all] - 关注订阅更新 / 全员提醒
|
|
23
|
+
rsso.edit <id> [选项] - 修改标题、URL、模板、选择器、目标
|
|
24
|
+
rsso.cache - 消息缓存管理
|
|
25
|
+
rsso.queue - 发送队列管理
|
|
26
|
+
|
|
27
|
+
## 网页监控相关:
|
|
28
|
+
rsso.html <url> -s <selector> - 使用 CSS 选择器监控网页
|
|
29
|
+
rsso.ask <url> <需求> - AI 生成选择器后创建网页订阅
|
|
30
|
+
rsso.watch <url> [关键词] - 简单网页 / 关键词监控
|
|
31
|
+
|
|
32
|
+
## 常用模板:
|
|
33
|
+
content - 纯文字正文
|
|
34
|
+
default - 默认图片模板
|
|
35
|
+
custom - 自定义模板
|
|
36
|
+
only text - 仅文本
|
|
37
|
+
only image - 仅图片
|
|
38
|
+
only media - 图片 + 视频
|
|
39
|
+
link - 跟随正文中的第一个链接
|
|
40
|
+
|
|
41
|
+
## 常用示例:
|
|
42
|
+
rsso https://example.com/rss
|
|
43
|
+
rsso -T tg:woshadiao
|
|
44
|
+
rsso https://example.com/rss -i content -t "示例订阅"
|
|
45
|
+
rsso.list
|
|
46
|
+
rsso.edit 1 -t "新标题"
|
|
47
|
+
rsso.html https://example.com -s ".news-item"
|
|
48
|
+
|
|
49
|
+
## 兼容提示:
|
|
50
|
+
旧选项 -l / -r / -f / -p 仍会返回迁移提示,
|
|
51
|
+
建议改用 rsso.list / rsso.remove / rsso.follow / rsso.pull。
|
|
89
52
|
|
|
90
53
|
</details>
|
|
91
54
|
`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare class AiSummaryCache {
|
|
2
|
+
private cache;
|
|
3
|
+
private ttl;
|
|
4
|
+
private maxSize;
|
|
5
|
+
private accessOrder;
|
|
6
|
+
constructor(ttl?: number, maxSize?: number);
|
|
7
|
+
private evictIfNeeded;
|
|
8
|
+
private updateAccessOrder;
|
|
9
|
+
private removeAccessOrder;
|
|
10
|
+
private generateKey;
|
|
11
|
+
get(title: string, content: string): string | null;
|
|
12
|
+
set(title: string, content: string, summary: string): void;
|
|
13
|
+
cleanExpired(): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
getStats(): {
|
|
16
|
+
size: number;
|
|
17
|
+
keys: string[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function initAiCache(ttl?: number, maxSize?: number): void;
|
|
21
|
+
export declare function getOrInitAiCache(ttl?: number, maxSize?: number): AiSummaryCache;
|
|
22
|
+
export declare function cleanExpiredCache(): void;
|
|
23
|
+
export declare function clearAiCache(): void;
|
|
24
|
+
export declare function getAiCacheStats(): {
|
|
25
|
+
size: number;
|
|
26
|
+
keys: string[];
|
|
27
|
+
} | null;
|
|
@@ -0,0 +1,169 @@
|
|
|
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.AiSummaryCache = void 0;
|
|
37
|
+
exports.initAiCache = initAiCache;
|
|
38
|
+
exports.getOrInitAiCache = getOrInitAiCache;
|
|
39
|
+
exports.cleanExpiredCache = cleanExpiredCache;
|
|
40
|
+
exports.clearAiCache = clearAiCache;
|
|
41
|
+
exports.getAiCacheStats = getAiCacheStats;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const logger_1 = require("../utils/logger");
|
|
44
|
+
class AiSummaryCache {
|
|
45
|
+
cache = new Map();
|
|
46
|
+
ttl;
|
|
47
|
+
maxSize;
|
|
48
|
+
accessOrder = [];
|
|
49
|
+
constructor(ttl = 24 * 60 * 60 * 1000, maxSize = 1000) {
|
|
50
|
+
this.ttl = ttl;
|
|
51
|
+
this.maxSize = maxSize;
|
|
52
|
+
}
|
|
53
|
+
evictIfNeeded() {
|
|
54
|
+
while (this.cache.size >= this.maxSize && this.accessOrder.length > 0) {
|
|
55
|
+
const oldestKey = this.accessOrder.shift();
|
|
56
|
+
if (oldestKey && this.cache.has(oldestKey)) {
|
|
57
|
+
this.cache.delete(oldestKey);
|
|
58
|
+
}
|
|
59
|
+
if (this.accessOrder.length === 0 && this.cache.size >= this.maxSize) {
|
|
60
|
+
const randomKey = this.cache.keys().next().value;
|
|
61
|
+
if (randomKey) {
|
|
62
|
+
this.cache.delete(randomKey);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
updateAccessOrder(key) {
|
|
69
|
+
const index = this.accessOrder.indexOf(key);
|
|
70
|
+
if (index > -1) {
|
|
71
|
+
this.accessOrder.splice(index, 1);
|
|
72
|
+
}
|
|
73
|
+
this.accessOrder.push(key);
|
|
74
|
+
}
|
|
75
|
+
removeAccessOrder(key) {
|
|
76
|
+
const index = this.accessOrder.indexOf(key);
|
|
77
|
+
if (index > -1) {
|
|
78
|
+
this.accessOrder.splice(index, 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
generateKey(title, content) {
|
|
82
|
+
return crypto
|
|
83
|
+
.createHash('sha256')
|
|
84
|
+
.update(`${title}|||${content}`)
|
|
85
|
+
.digest('hex');
|
|
86
|
+
}
|
|
87
|
+
get(title, content) {
|
|
88
|
+
const key = this.generateKey(title, content);
|
|
89
|
+
const entry = this.cache.get(key);
|
|
90
|
+
if (!entry)
|
|
91
|
+
return null;
|
|
92
|
+
if (Date.now() - entry.timestamp > this.ttl) {
|
|
93
|
+
this.cache.delete(key);
|
|
94
|
+
this.removeAccessOrder(key);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
this.updateAccessOrder(key);
|
|
98
|
+
entry.lastAccess = Date.now();
|
|
99
|
+
return entry.summary;
|
|
100
|
+
}
|
|
101
|
+
set(title, content, summary) {
|
|
102
|
+
const key = this.generateKey(title, content);
|
|
103
|
+
if (this.cache.has(key)) {
|
|
104
|
+
this.updateAccessOrder(key);
|
|
105
|
+
const entry = this.cache.get(key);
|
|
106
|
+
entry.summary = summary;
|
|
107
|
+
entry.timestamp = Date.now();
|
|
108
|
+
entry.lastAccess = Date.now();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.evictIfNeeded();
|
|
112
|
+
this.cache.set(key, {
|
|
113
|
+
summary,
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
lastAccess: Date.now()
|
|
116
|
+
});
|
|
117
|
+
this.accessOrder.push(key);
|
|
118
|
+
}
|
|
119
|
+
cleanExpired() {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
122
|
+
if (now - entry.timestamp > this.ttl) {
|
|
123
|
+
this.cache.delete(key);
|
|
124
|
+
this.removeAccessOrder(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
clear() {
|
|
129
|
+
this.cache.clear();
|
|
130
|
+
this.accessOrder = [];
|
|
131
|
+
}
|
|
132
|
+
getStats() {
|
|
133
|
+
return {
|
|
134
|
+
size: this.cache.size,
|
|
135
|
+
keys: Array.from(this.cache.keys())
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.AiSummaryCache = AiSummaryCache;
|
|
140
|
+
let globalCache = null;
|
|
141
|
+
function initAiCache(ttl, maxSize) {
|
|
142
|
+
if (!globalCache) {
|
|
143
|
+
const defaultMaxSize = 1000;
|
|
144
|
+
globalCache = new AiSummaryCache(ttl, maxSize || defaultMaxSize);
|
|
145
|
+
(0, logger_1.debug)({ debug: 'info' }, `AI 摘要缓存已初始化 (TTL: ${ttl || 24 * 60 * 60 * 1000}ms, MaxSize: ${maxSize || defaultMaxSize})`, 'AI-Cache', 'info');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function getOrInitAiCache(ttl, maxSize) {
|
|
149
|
+
if (!globalCache) {
|
|
150
|
+
initAiCache(ttl, maxSize);
|
|
151
|
+
}
|
|
152
|
+
return globalCache;
|
|
153
|
+
}
|
|
154
|
+
function cleanExpiredCache() {
|
|
155
|
+
if (globalCache) {
|
|
156
|
+
globalCache.cleanExpired();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function clearAiCache() {
|
|
160
|
+
if (globalCache) {
|
|
161
|
+
globalCache.clear();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function getAiCacheStats() {
|
|
165
|
+
if (globalCache) {
|
|
166
|
+
return globalCache.getStats();
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Config } from '../types';
|
|
2
|
+
export interface SummaryResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
summary: string;
|
|
5
|
+
cached: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function requestAiText(config: Config, prompt: string, options?: {
|
|
9
|
+
temperature?: number;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}): Promise<string>;
|
|
12
|
+
export declare function callAiApi(config: Config, prompt: string, context: string): Promise<SummaryResult>;
|