@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.
Files changed (90) hide show
  1. package/README.md +92 -37
  2. package/lib/commands/error-handler.js +13 -4
  3. package/lib/commands/index.d.ts +20 -1
  4. package/lib/commands/index.js +394 -2
  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/subscription-edit.d.ts +7 -0
  10. package/lib/commands/subscription-edit.js +177 -0
  11. package/lib/commands/subscription-management.d.ts +12 -0
  12. package/lib/commands/subscription-management.js +176 -0
  13. package/lib/commands/utils.d.ts +13 -1
  14. package/lib/commands/utils.js +43 -2
  15. package/lib/commands/web-monitor.d.ts +15 -0
  16. package/lib/commands/web-monitor.js +222 -0
  17. package/lib/config.js +25 -0
  18. package/lib/constants.d.ts +1 -1
  19. package/lib/constants.js +46 -83
  20. package/lib/core/ai-cache.d.ts +27 -0
  21. package/lib/core/ai-cache.js +169 -0
  22. package/lib/core/ai-client.d.ts +12 -0
  23. package/lib/core/ai-client.js +65 -0
  24. package/lib/core/ai-selector.d.ts +2 -0
  25. package/lib/core/ai-selector.js +80 -0
  26. package/lib/core/ai-summary.d.ts +10 -0
  27. package/lib/core/ai-summary.js +73 -0
  28. package/lib/core/ai-utils.d.ts +10 -0
  29. package/lib/core/ai-utils.js +104 -0
  30. package/lib/core/ai.d.ts +3 -77
  31. package/lib/core/ai.js +13 -455
  32. package/lib/core/feeder-arg.d.ts +17 -0
  33. package/lib/core/feeder-arg.js +234 -0
  34. package/lib/core/feeder-runtime.d.ts +96 -0
  35. package/lib/core/feeder-runtime.js +233 -0
  36. package/lib/core/feeder.d.ts +4 -6
  37. package/lib/core/feeder.js +120 -304
  38. package/lib/core/item-processor-runtime.d.ts +46 -0
  39. package/lib/core/item-processor-runtime.js +215 -0
  40. package/lib/core/item-processor-template.d.ts +16 -0
  41. package/lib/core/item-processor-template.js +158 -0
  42. package/lib/core/item-processor.d.ts +1 -10
  43. package/lib/core/item-processor.js +48 -393
  44. package/lib/core/notification-queue-retry.d.ts +25 -0
  45. package/lib/core/notification-queue-retry.js +78 -0
  46. package/lib/core/notification-queue-sender.d.ts +20 -0
  47. package/lib/core/notification-queue-sender.js +118 -0
  48. package/lib/core/notification-queue-store.d.ts +19 -0
  49. package/lib/core/notification-queue-store.js +137 -0
  50. package/lib/core/notification-queue-types.d.ts +49 -0
  51. package/lib/core/notification-queue-types.js +2 -0
  52. package/lib/core/notification-queue.d.ts +13 -72
  53. package/lib/core/notification-queue.js +132 -262
  54. package/lib/core/parser.js +12 -0
  55. package/lib/core/renderer.d.ts +15 -0
  56. package/lib/core/renderer.js +91 -23
  57. package/lib/core/search-format.d.ts +3 -0
  58. package/lib/core/search-format.js +36 -0
  59. package/lib/core/search-providers.d.ts +13 -0
  60. package/lib/core/search-providers.js +175 -0
  61. package/lib/core/search-rotation.d.ts +4 -0
  62. package/lib/core/search-rotation.js +55 -0
  63. package/lib/core/search-service.d.ts +3 -0
  64. package/lib/core/search-service.js +100 -0
  65. package/lib/core/search-types.d.ts +39 -0
  66. package/lib/core/search-types.js +2 -0
  67. package/lib/core/search.d.ts +4 -101
  68. package/lib/core/search.js +10 -508
  69. package/lib/index.js +50 -1160
  70. package/lib/tsconfig.tsbuildinfo +1 -1
  71. package/lib/types.d.ts +51 -6
  72. package/lib/utils/common.js +52 -3
  73. package/lib/utils/error-handler.d.ts +8 -0
  74. package/lib/utils/error-handler.js +27 -0
  75. package/lib/utils/error-tracker.js +24 -8
  76. package/lib/utils/fetcher.js +68 -9
  77. package/lib/utils/legacy-config.d.ts +12 -0
  78. package/lib/utils/legacy-config.js +56 -0
  79. package/lib/utils/logger.d.ts +4 -2
  80. package/lib/utils/logger.js +193 -34
  81. package/lib/utils/media.js +3 -6
  82. package/lib/utils/proxy.d.ts +3 -0
  83. package/lib/utils/proxy.js +14 -0
  84. package/lib/utils/sanitizer.d.ts +58 -0
  85. package/lib/utils/sanitizer.js +227 -0
  86. package/lib/utils/security.d.ts +75 -0
  87. package/lib/utils/security.js +312 -0
  88. package/lib/utils/structured-logger.d.ts +7 -3
  89. package/lib/utils/structured-logger.js +29 -39
  90. 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
  });
@@ -1,4 +1,4 @@
1
- export declare const usage = "\n<details>\n<summary>RSS-OWL \u8BA2\u9605\u5668\u4F7F\u7528\u8BF4\u660E</summary>\n\n## \u57FA\u672C\u547D\u4EE4:\n rsso &lt;url&gt; - \u8BA2\u9605RSS\u94FE\u63A5\n rsso -l - \u67E5\u770B\u8BA2\u9605\u5217\u8868\n rsso -l [id] - \u67E5\u770B\u8BA2\u9605\u8BE6\u60C5\n rsso -r &lt;content&gt; - \u5220\u9664\u8BA2\u9605(\u9700\u8981\u6743\u9650)\n rsso -T &lt;url&gt; - \u6D4B\u8BD5\u8BA2\u9605\n rsso.ask &lt;url&gt; &lt;\u9700\u6C42&gt; - AI \u667A\u80FD\u8BA2\u9605\u7F51\u9875 (\u9700\u8981 AI \u914D\u7F6E)\n rsso.watch &lt;url&gt; [\u5173\u952E\u8BCD] - \u7B80\u5355\u7F51\u9875\u76D1\u63A7 (\u5173\u952E\u8BCD/\u6574\u9875)\n\n## \u5E38\u7528\u9009\u9879:\n -i &lt;template&gt; - \u8BBE\u7F6E\u6D88\u606F\u6A21\u677F\n \u53EF\u9009\u503C: content(\u6587\u5B57) | default(\u56FE\u7247) | custom(\u81EA\u5B9A\u4E49) | only text | only media \u7B49\n -t &lt;title&gt; - \u81EA\u5B9A\u4E49\u8BA2\u9605\u6807\u9898\n -a &lt;arg&gt; - \u81EA\u5B9A\u4E49\u914D\u7F6E (\u683C\u5F0F: key:value,key2:value2)\n \u4F8B\u5982: -a timeout:30,merge:true\n\n## \u9AD8\u7EA7\u9009\u9879:\n -f &lt;content&gt; - \u5173\u6CE8\u8BA2\u9605\uFF0C\u66F4\u65B0\u65F6\u63D0\u9192\n -fAll &lt;content&gt; - \u5168\u4F53\u5173\u6CE8(\u9700\u8981\u9AD8\u7EA7\u6743\u9650)\n -target &lt;groupId&gt; - \u8DE8\u7FA4\u8BA2\u9605(\u9700\u8981\u9AD8\u7EA7\u6743\u9650)\n -d &lt;time&gt; - \u5B9A\u65F6\u63A8\u9001 (\u683C\u5F0F: \"HH:mm/\u6570\u91CF\" \u6216 \"HH:mm\")\n \u4F8B\u5982: -d \"08:00/5\" \u8868\u793A\u6BCF\u59298\u70B9\u63A8\u90015\u6761\n -p &lt;id&gt; - \u624B\u52A8\u62C9\u53D6\u6700\u65B0\u5185\u5BB9\n\n## \u5FEB\u901F\u8BA2\u9605:\n rsso -q - \u67E5\u770B\u5FEB\u901F\u8BA2\u9605\u5217\u8868\n rsso -q [\u7F16\u53F7] - \u67E5\u770B\u5FEB\u901F\u8BA2\u9605\u8BE6\u60C5\n rsso -T tg:channel_name - \u5FEB\u901F\u8BA2\u9605Telegram\u9891\u9053\n\n## Assets \u56FE\u7247/\u89C6\u9891\u670D\u52A1\u914D\u7F6E (\u63A8\u8350):\n \u4F7F\u7528 assets \u670D\u52A1\u53EF\u4EE5\u907F\u514D Base64 \u8D85\u957F\u95EE\u9898\n 1. \u5728\u63D2\u4EF6\u5E02\u573A\u5B89\u88C5 assets-xxx \u63D2\u4EF6 (\u5982 assets-local, assets-s3, assets-smms \u7B49)\n 2. \u5728\u5BF9\u5E94\u63D2\u4EF6\u4E2D\u914D\u7F6E\u5B58\u50A8\u4FE1\u606F (AccessKey, Secret, Bucket \u7B49)\n 3. \u5728 RSS-Owl \u57FA\u7840\u8BBE\u7F6E\u4E2D\u5C06 imageMode/videoMode \u8BBE\u7F6E\u4E3A 'assets'\n 4. \u63D2\u4EF6\u4F1A\u81EA\u52A8\u4E0A\u4F20\u56FE\u7247/\u89C6\u9891\u5230\u4F60\u7684\u56FE\u5E8A\u670D\u52A1\n\n## \u914D\u7F6E\u793A\u4F8B:\n rsso -T -i content \"https://example.com/rss\"\n rsso \"https://example.com/rss\" -t \"\u6211\u7684\u8BA2\u9605\" -a \"timeout:60,merge:true\"\n rsso -d \"09:00/3\" \"https://example.com/rss\"\n\n</details>\n\n<details>\n<summary>\u7F51\u9875\u76D1\u63A7 (rsso.html) - \u76D1\u63A7\u4EFB\u610F\u7F51\u9875\u5143\u7D20\u53D8\u5316</summary>\n\n\u4F7F\u7528 CSS \u9009\u62E9\u5668\u76D1\u63A7\u7F51\u9875\u5143\u7D20\u53D8\u5316\uFF0C\u652F\u6301\u9759\u6001\u7F51\u9875\u548C SPA \u52A8\u6001\u9875\u9762\u3002\n\n## \u57FA\u672C\u7528\u6CD5:\n rsso.html &lt;url&gt; -s &lt;selector&gt; - \u76D1\u63A7\u7B26\u5408\u9009\u62E9\u5668\u7684\u5143\u7D20\n\n## \u5E38\u7528\u9009\u9879:\n -s, --selector &lt;\u9009\u62E9\u5668&gt; CSS \u9009\u62E9\u5668 (\u5FC5\u586B)\uFF0C\u4F8B\u5982: .news-item\u3001#price\u3001div.list > li\n -t, --title &lt;\u6807\u9898&gt; \u81EA\u5B9A\u4E49\u8BA2\u9605\u6807\u9898\n -i, --template &lt;\u6A21\u677F&gt; \u6D88\u606F\u6A21\u677F (\u63A8\u8350 content)\n --text \u53EA\u63D0\u53D6\u7EAF\u6587\u672C (\u9ED8\u8BA4\u63D0\u53D6 HTML)\n -T, --test \u6D4B\u8BD5\u6A21\u5F0F\uFF0C\u67E5\u770B\u6293\u53D6\u9884\u89C8\n\n## Puppeteer \u52A8\u6001\u6E32\u67D3 (\u89E3\u51B3 SPA/JS \u52A8\u6001\u5185\u5BB9):\n -P, --puppeteer \u4F7F\u7528 Puppeteer \u6E32\u67D3\u9875\u9762 (\u9700\u8981\u5B89\u88C5 koishi-plugin-puppeteer)\n -w, --wait &lt;\u6BEB\u79D2&gt; \u6E32\u67D3\u540E\u7B49\u5F85\u65F6\u95F4\n -W, --waitSelector &lt;\u9009\u62E9\u5668&gt; \u7B49\u5F85\u7279\u5B9A\u5143\u7D20\u51FA\u73B0\n\n</details>\n\n<details>\n<summary>AI \u6458\u8981 (ai) - \u667A\u80FD\u751F\u6210\u5185\u5BB9\u6458\u8981</summary>\n\n\u4F7F\u7528 OpenAI \u517C\u5BB9 API \u4E3A\u8BA2\u9605\u5185\u5BB9\u751F\u6210 AI \u6458\u8981\u3002\n\n## \u542F\u7528\u65B9\u6CD5:\n 1. \u5728\u63D2\u4EF6\u914D\u7F6E\u4E2D\u5F00\u542F AI \u529F\u80FD\n 2. \u586B\u5199 API Base URL\u3001API Key \u548C\u6A21\u578B\u540D\u79F0\n\n## \u914D\u7F6E\u9879:\n - placement \u6458\u8981\u4F4D\u7F6E: top (\u9876\u90E8) / bottom (\u5E95\u90E8)\n - separator \u5206\u5272\u7EBF\u6837\u5F0F\n - prompt \u63D0\u793A\u8BCD\u6A21\u677F ({{title}} \u6807\u9898, {{content}} \u5185\u5BB9)\n - maxInputLength \u6700\u5927\u8F93\u5165\u957F\u5EA6 (\u9ED8\u8BA4 2000 \u5B57)\n - timeout \u8BF7\u6C42\u8D85\u65F6 (\u9ED8\u8BA4 30000 \u6BEB\u79D2)\n\n\n</details>\n";
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 &lt;url&gt; - \u521B\u5EFA RSS / Atom / JSON Feed \u8BA2\u9605\n rsso -T &lt;url&gt; - \u6D4B\u8BD5\u6293\u53D6\uFF0C\u4E0D\u5199\u5165\u8BA2\u9605\n rsso &lt;url&gt; -t &lt;\u6807\u9898&gt; - \u81EA\u5B9A\u4E49\u8BA2\u9605\u6807\u9898\n rsso &lt;url&gt; -i &lt;\u6A21\u677F&gt; - \u6307\u5B9A\u6D88\u606F\u6A21\u677F\n rsso &lt;url&gt; -a &lt;key:value,...&gt; - \u8986\u76D6\u8BA2\u9605\u53C2\u6570\n rsso &lt;url&gt; -d &lt;HH:mm[/\u6570\u91CF]&gt; - \u6BCF\u65E5\u5B9A\u65F6\u63A8\u9001\n rsso &lt;url&gt; --target &lt;\u5E73\u53F0:\u9891\u9053&gt; - \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 &lt;id&gt; [--all] - \u5220\u9664\u8BA2\u9605 / \u5220\u9664\u5168\u90E8\n rsso.pull &lt;id&gt; - \u62C9\u53D6\u8BA2\u9605\u6700\u65B0\u5185\u5BB9\n rsso.follow &lt;id&gt; [--all] - \u5173\u6CE8\u8BA2\u9605\u66F4\u65B0 / \u5168\u5458\u63D0\u9192\n rsso.edit &lt;id&gt; [\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 &lt;url&gt; -s &lt;selector&gt; - \u4F7F\u7528 CSS \u9009\u62E9\u5668\u76D1\u63A7\u7F51\u9875\n rsso.ask &lt;url&gt; &lt;\u9700\u6C42&gt; - AI \u751F\u6210\u9009\u62E9\u5668\u540E\u521B\u5EFA\u7F51\u9875\u8BA2\u9605\n rsso.watch &lt;url&gt; [\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 订阅器使用说明</summary>
7
-
8
- ## 基本命令:
9
- rsso &lt;url&gt; - 订阅RSS链接
10
- rsso -l - 查看订阅列表
11
- rsso -l [id] - 查看订阅详情
12
- rsso -r &lt;content&gt; - 删除订阅(需要权限)
13
- rsso -T &lt;url&gt; - 测试订阅
14
- rsso.ask &lt;url&gt; &lt;需求&gt; - AI 智能订阅网页 (需要 AI 配置)
15
- rsso.watch &lt;url&gt; [关键词] - 简单网页监控 (关键词/整页)
16
-
17
- ## 常用选项:
18
- -i &lt;template&gt; - 设置消息模板
19
- 可选值: content(文字) | default(图片) | custom(自定义) | only text | only media 等
20
- -t &lt;title&gt; - 自定义订阅标题
21
- -a &lt;arg&gt; - 自定义配置 (格式: key:value,key2:value2)
22
- 例如: -a timeout:30,merge:true
23
-
24
- ## 高级选项:
25
- -f &lt;content&gt; - 关注订阅,更新时提醒
26
- -fAll &lt;content&gt; - 全体关注(需要高级权限)
27
- -target &lt;groupId&gt; - 跨群订阅(需要高级权限)
28
- -d &lt;time&gt; - 定时推送 (格式: "HH:mm/数量" 或 "HH:mm")
29
- 例如: -d "08:00/5" 表示每天8点推送5条
30
- -p &lt;id&gt; - 手动拉取最新内容
31
-
32
- ## 快速订阅:
33
- rsso -q - 查看快速订阅列表
34
- rsso -q [编号] - 查看快速订阅详情
35
- rsso -T tg:channel_name - 快速订阅Telegram频道
36
-
37
- ## Assets 图片/视频服务配置 (推荐):
38
- 使用 assets 服务可以避免 Base64 超长问题
39
- 1. 在插件市场安装 assets-xxx 插件 (如 assets-local, assets-s3, assets-smms 等)
40
- 2. 在对应插件中配置存储信息 (AccessKey, Secret, Bucket 等)
41
- 3. 在 RSS-Owl 基础设置中将 imageMode/videoMode 设置为 'assets'
42
- 4. 插件会自动上传图片/视频到你的图床服务
43
-
44
- ## 配置示例:
45
- rsso -T -i content "https://example.com/rss"
46
- rsso "https://example.com/rss" -t "我的订阅" -a "timeout:60,merge:true"
47
- rsso -d "09:00/3" "https://example.com/rss"
48
-
49
- </details>
50
-
51
- <details>
52
- <summary>网页监控 (rsso.html) - 监控任意网页元素变化</summary>
53
-
54
- 使用 CSS 选择器监控网页元素变化,支持静态网页和 SPA 动态页面。
55
-
56
- ## 基本用法:
57
- rsso.html &lt;url&gt; -s &lt;selector&gt; - 监控符合选择器的元素
58
-
59
- ## 常用选项:
60
- -s, --selector &lt;选择器&gt; CSS 选择器 (必填),例如: .news-item、#price、div.list > li
61
- -t, --title &lt;标题&gt; 自定义订阅标题
62
- -i, --template &lt;模板&gt; 消息模板 (推荐 content)
63
- --text 只提取纯文本 (默认提取 HTML)
64
- -T, --test 测试模式,查看抓取预览
65
-
66
- ## Puppeteer 动态渲染 (解决 SPA/JS 动态内容):
67
- -P, --puppeteer 使用 Puppeteer 渲染页面 (需要安装 koishi-plugin-puppeteer)
68
- -w, --wait &lt;毫秒&gt; 渲染后等待时间
69
- -W, --waitSelector &lt;选择器&gt; 等待特定元素出现
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 &lt;url&gt; - 创建 RSS / Atom / JSON Feed 订阅
10
+ rsso -T &lt;url&gt; - 测试抓取,不写入订阅
11
+ rsso &lt;url&gt; -t &lt;标题&gt; - 自定义订阅标题
12
+ rsso &lt;url&gt; -i &lt;模板&gt; - 指定消息模板
13
+ rsso &lt;url&gt; -a &lt;key:value,...&gt; - 覆盖订阅参数
14
+ rsso &lt;url&gt; -d &lt;HH:mm[/数量]&gt; - 每日定时推送
15
+ rsso &lt;url&gt; --target &lt;平台:频道&gt; - 跨群 / 跨频道订阅(高级权限)
16
+ rsso -q [编号] - 查看快速订阅列表 / 详情
17
+
18
+ ## 管理订阅(使用列表序号):
19
+ rsso.list [id] - 查看订阅列表 / 详情
20
+ rsso.remove &lt;id&gt; [--all] - 删除订阅 / 删除全部
21
+ rsso.pull &lt;id&gt; - 拉取订阅最新内容
22
+ rsso.follow &lt;id&gt; [--all] - 关注订阅更新 / 全员提醒
23
+ rsso.edit &lt;id&gt; [选项] - 修改标题、URL、模板、选择器、目标
24
+ rsso.cache - 消息缓存管理
25
+ rsso.queue - 发送队列管理
26
+
27
+ ## 网页监控相关:
28
+ rsso.html &lt;url&gt; -s &lt;selector&gt; - 使用 CSS 选择器监控网页
29
+ rsso.ask &lt;url&gt; &lt;需求&gt; - AI 生成选择器后创建网页订阅
30
+ rsso.watch &lt;url&gt; [关键词] - 简单网页 / 关键词监控
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>;