@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
package/lib/commands/index.js
CHANGED
|
@@ -1,4 +1,396 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Commands module placeholder - commands are registered in the main index.ts for now
|
|
3
|
-
// This file can be used to export command registration functions in future refactoring
|
|
4
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCommandRuntimeDeps = exports.registerWebMonitorCommands = exports.registerSubscriptionCreateCommand = exports.registerSubscriptionEditCommand = exports.registerSubscriptionManagementCommands = void 0;
|
|
4
|
+
exports.registerManagementCommands = registerManagementCommands;
|
|
5
|
+
const error_handler_1 = require("../utils/error-handler");
|
|
6
|
+
const error_tracker_1 = require("../utils/error-tracker");
|
|
7
|
+
const message_cache_1 = require("../utils/message-cache");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
/**
|
|
11
|
+
* 注册管理类命令
|
|
12
|
+
*/
|
|
13
|
+
function registerManagementCommands(deps) {
|
|
14
|
+
registerCacheCommands(deps.ctx, deps.config);
|
|
15
|
+
registerQueueCommands(deps.ctx, deps.config, deps.queueManager);
|
|
16
|
+
}
|
|
17
|
+
function registerCacheCommands(ctx, config) {
|
|
18
|
+
ctx.guild()
|
|
19
|
+
.command('rssowl.cache', '消息缓存管理')
|
|
20
|
+
.alias('rsso.cache')
|
|
21
|
+
.usage(`
|
|
22
|
+
消息缓存管理功能,查看和管理已推送的 RSS 消息缓存。
|
|
23
|
+
|
|
24
|
+
用法:
|
|
25
|
+
rsso.cache list [页数] - 查看缓存消息列表
|
|
26
|
+
rsso.cache search <关键词> - 搜索缓存消息
|
|
27
|
+
rsso.cache stats - 查看缓存统计
|
|
28
|
+
rsso.cache message <序号> - 查看消息详情
|
|
29
|
+
rsso.cache pull <序号> - 重新推送缓存消息
|
|
30
|
+
rsso.cache clear - 清空所有缓存
|
|
31
|
+
rsso.cache cleanup [保留数量] - 清理缓存(保留最新N条)
|
|
32
|
+
|
|
33
|
+
示例:
|
|
34
|
+
rsso.cache list - 查看第1页(每页10条)
|
|
35
|
+
rsso.cache list 2 - 查看第2页
|
|
36
|
+
rsso.cache search 新闻 - 搜索包含"新闻"的消息
|
|
37
|
+
rsso.cache stats - 查看统计信息
|
|
38
|
+
rsso.cache message 1 - 查看序号1的消息详情
|
|
39
|
+
rsso.cache pull 1 - 推送序号1的消息
|
|
40
|
+
rsso.cache cleanup 50 - 清理并保留最新50条
|
|
41
|
+
|
|
42
|
+
注意:序号从1开始,会在列表中显示对应的真实数据库ID
|
|
43
|
+
`)
|
|
44
|
+
.action(async ({ session }, subcommand, ...args) => {
|
|
45
|
+
const { authority } = session.user;
|
|
46
|
+
const cache = (0, message_cache_1.getMessageCache)();
|
|
47
|
+
if (!cache) {
|
|
48
|
+
return '消息缓存功能未启用,请在配置中启用 cache.enabled';
|
|
49
|
+
}
|
|
50
|
+
if (!subcommand) {
|
|
51
|
+
return `消息缓存管理
|
|
52
|
+
|
|
53
|
+
可用指令:
|
|
54
|
+
rsso.cache list [页数] - 查看缓存消息列表
|
|
55
|
+
rsso.cache search <关键词> - 搜索缓存消息
|
|
56
|
+
rsso.cache stats - 查看缓存统计
|
|
57
|
+
rsso.cache message <序号> - 查看消息详情
|
|
58
|
+
rsso.cache pull <序号> - 重新推送缓存消息
|
|
59
|
+
rsso.cache clear - 清空所有缓存
|
|
60
|
+
rsso.cache cleanup [保留数量] - 清理缓存(保留最新N条)
|
|
61
|
+
|
|
62
|
+
详细信息请使用: rsso.cache --help`;
|
|
63
|
+
}
|
|
64
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.cache', subcommand);
|
|
65
|
+
switch (subcommand) {
|
|
66
|
+
case 'list':
|
|
67
|
+
return handleCacheList(cache, args, config, logContext);
|
|
68
|
+
case 'message':
|
|
69
|
+
return handleCacheMessage(cache, args, config, logContext);
|
|
70
|
+
case 'search':
|
|
71
|
+
return handleCacheSearch(cache, args, config, logContext);
|
|
72
|
+
case 'stats':
|
|
73
|
+
return handleCacheStats(cache, config, logContext);
|
|
74
|
+
case 'clear':
|
|
75
|
+
if (authority < config.basic.authority) {
|
|
76
|
+
return `权限不足!当前权限: ${authority},需要权限: ${config.basic.authority} 或以上`;
|
|
77
|
+
}
|
|
78
|
+
return handleCacheClear(cache, config, logContext);
|
|
79
|
+
case 'cleanup':
|
|
80
|
+
if (authority < config.basic.authority) {
|
|
81
|
+
return `权限不足!当前权限: ${authority},需要权限: ${config.basic.authority} 或以上`;
|
|
82
|
+
}
|
|
83
|
+
return handleCacheCleanup(cache, args, config, logContext);
|
|
84
|
+
case 'pull':
|
|
85
|
+
return handleCachePull(ctx, session, cache, args, config, logContext);
|
|
86
|
+
default:
|
|
87
|
+
return `未知的子命令: ${subcommand}\n使用 "rsso.cache" 查看可用指令`;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function registerQueueCommands(ctx, config, queueManager) {
|
|
92
|
+
ctx.guild()
|
|
93
|
+
.command('rssowl.queue', '发送队列管理')
|
|
94
|
+
.alias('rsso.queue')
|
|
95
|
+
.usage(`
|
|
96
|
+
发送队列管理功能,查看和管理待发送的消息队列。
|
|
97
|
+
|
|
98
|
+
用法:
|
|
99
|
+
rsso.queue stats - 查看队列统计
|
|
100
|
+
rsso.queue retry [id] - 重试失败的任务
|
|
101
|
+
rsso.queue retry --all - 重试所有失败任务
|
|
102
|
+
rsso.queue cleanup [hours] - 清理旧的成功任务(默认24小时)
|
|
103
|
+
|
|
104
|
+
示例:
|
|
105
|
+
rsso.queue stats - 查看队列状态
|
|
106
|
+
rsso.queue retry 5 - 重试ID为5的任务
|
|
107
|
+
rsso.queue retry --all - 重试所有失败任务
|
|
108
|
+
rsso.queue cleanup 48 - 清理48小时前的成功任务
|
|
109
|
+
|
|
110
|
+
说明:
|
|
111
|
+
- PENDING: 待发送
|
|
112
|
+
- RETRY: 等待重试
|
|
113
|
+
- FAILED: 发送失败
|
|
114
|
+
- SUCCESS: 发送成功
|
|
115
|
+
`)
|
|
116
|
+
.action(async ({ session }, subcommand, ...args) => {
|
|
117
|
+
const { authority } = session.user;
|
|
118
|
+
if (!subcommand) {
|
|
119
|
+
return `发送队列管理
|
|
120
|
+
|
|
121
|
+
可用指令:
|
|
122
|
+
rsso.queue stats - 查看队列统计
|
|
123
|
+
rsso.queue retry [id] - 重试失败的任务
|
|
124
|
+
rsso.queue retry --all - 重试所有失败任务
|
|
125
|
+
rsso.queue cleanup [hours] - 清理旧的成功任务(默认24小时)
|
|
126
|
+
|
|
127
|
+
详细信息请使用: rsso.queue --help`;
|
|
128
|
+
}
|
|
129
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.queue', subcommand);
|
|
130
|
+
switch (subcommand) {
|
|
131
|
+
case 'stats':
|
|
132
|
+
return handleQueueStats(queueManager, config, logContext);
|
|
133
|
+
case 'retry':
|
|
134
|
+
if (authority < config.basic.authority) {
|
|
135
|
+
return `权限不足!当前权限: ${authority},需要权限: ${config.basic.authority} 或以上`;
|
|
136
|
+
}
|
|
137
|
+
return handleQueueRetry(queueManager, args, config, logContext);
|
|
138
|
+
case 'cleanup':
|
|
139
|
+
if (authority < config.basic.authority) {
|
|
140
|
+
return `权限不足!当前权限: ${authority},需要权限: ${config.basic.authority} 或以上`;
|
|
141
|
+
}
|
|
142
|
+
return handleQueueCleanup(queueManager, args, config, logContext);
|
|
143
|
+
default:
|
|
144
|
+
return `未知的子命令: ${subcommand}\n使用 "rsso.queue" 查看可用指令`;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function handleCacheList(cache, args, config, logContext) {
|
|
149
|
+
const page = parseInt(args[0]) || 1;
|
|
150
|
+
const limit = 10;
|
|
151
|
+
const offset = (page - 1) * limit;
|
|
152
|
+
try {
|
|
153
|
+
const messages = await cache.getMessages({ limit, offset });
|
|
154
|
+
if (messages.length === 0) {
|
|
155
|
+
return '暂无缓存消息';
|
|
156
|
+
}
|
|
157
|
+
const stats = await cache.getStats();
|
|
158
|
+
let output = `📋 缓存消息列表 (第${page}页,共${Math.ceil(stats.totalMessages / limit)}页,总计${stats.totalMessages}条)\n\n`;
|
|
159
|
+
output += messages.map((msg, index) => {
|
|
160
|
+
const date = new Date(msg.createdAt).toLocaleString('zh-CN', {
|
|
161
|
+
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
162
|
+
});
|
|
163
|
+
const title = msg.title.length > 30 ? msg.title.substring(0, 30) + '...' : msg.title;
|
|
164
|
+
return `${index + 1}. [ID:${msg.id}] [${msg.rssId}] ${title}\n 时间: ${date}\n 链接: ${msg.link}`;
|
|
165
|
+
}).join('\n\n');
|
|
166
|
+
output += `\n\n💡 使用 "rsso.cache list ${page + 1}" 查看下一页`;
|
|
167
|
+
output += '\n💡 使用 "rsso.cache pull <序号>" 推送消息(注意:序号基于当前页)';
|
|
168
|
+
output += '\n💡 使用 "rsso.cache message <序号>" 查看详情';
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
logCommandError(config, error, 'cache list error', { ...logContext, page, limit });
|
|
173
|
+
return `获取消息列表失败: ${error.message}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function handleCacheMessage(cache, args, config, logContext) {
|
|
177
|
+
const serialNumber = parseInt(args[0]);
|
|
178
|
+
if (!serialNumber || serialNumber < 1) {
|
|
179
|
+
return '请提供序号\n使用方法: rsso.cache message <序号>\n示例: rsso.cache message 1\n💡 提示:使用 "rsso.cache list" 查看序号';
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const found = await findCachedMessageBySerial(cache, serialNumber);
|
|
183
|
+
if (!found.message) {
|
|
184
|
+
return `❌ 未找到序号为 ${args[0]} 的消息\n💡 使用 "rsso.cache list" 查看可用的序号`;
|
|
185
|
+
}
|
|
186
|
+
const pubDate = new Date(found.message.pubDate).toLocaleString('zh-CN', {
|
|
187
|
+
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
188
|
+
});
|
|
189
|
+
const createdAt = new Date(found.message.createdAt).toLocaleString('zh-CN', {
|
|
190
|
+
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
191
|
+
});
|
|
192
|
+
let output = `📰 消息详情 (第${found.page}页序号${args[0]},真实ID:${found.message.id})\n\n`;
|
|
193
|
+
output += `📰 标题: ${found.message.title}\n📡 订阅: ${found.message.rssId}\n👥 群组: ${found.message.platform}:${found.message.guildId}\n🔗 链接: ${found.message.link}\n📅 发布时间: ${pubDate}\n💾 缓存时间: ${createdAt}\n`;
|
|
194
|
+
if (found.message.content) {
|
|
195
|
+
const content = found.message.content.length > 200 ? found.message.content.substring(0, 200) + '...' : found.message.content;
|
|
196
|
+
output += `\n📝 内容:\n${content}`;
|
|
197
|
+
}
|
|
198
|
+
if (found.message.imageUrl)
|
|
199
|
+
output += `\n\n🖼️ 图片: ${found.message.imageUrl}`;
|
|
200
|
+
if (found.message.videoUrl)
|
|
201
|
+
output += `\n\n🎬 视频: ${found.message.videoUrl}`;
|
|
202
|
+
return output;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
logCommandError(config, error, 'cache message error', { ...logContext, serialNumber });
|
|
206
|
+
return `获取消息详情失败: ${error.message}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function handleCacheSearch(cache, args, config, logContext) {
|
|
210
|
+
const keyword = args[0];
|
|
211
|
+
if (!keyword) {
|
|
212
|
+
return '请提供搜索关键词\n使用方法: rsso.cache search <关键词>';
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const messages = await cache.searchMessages({ keyword, limit: 10 });
|
|
216
|
+
if (messages.length === 0) {
|
|
217
|
+
return `未找到包含 "${keyword}" 的消息`;
|
|
218
|
+
}
|
|
219
|
+
let output = `🔍 搜索结果 "${keyword}" (找到${messages.length}条)\n\n`;
|
|
220
|
+
output += messages.map((msg, index) => {
|
|
221
|
+
const date = new Date(msg.createdAt).toLocaleString('zh-CN', {
|
|
222
|
+
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
223
|
+
});
|
|
224
|
+
const title = msg.title.length > 30 ? msg.title.substring(0, 30) + '...' : msg.title;
|
|
225
|
+
return `${index + 1}. [ID:${msg.id}] [${msg.rssId}] ${title}\n 时间: ${date}`;
|
|
226
|
+
}).join('\n\n');
|
|
227
|
+
output += '\n\n💡 使用 "rsso.cache message <真实ID>" 查看详情';
|
|
228
|
+
return output;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
logCommandError(config, error, 'cache search error', { ...logContext, keyword });
|
|
232
|
+
return `搜索失败: ${error.message}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function handleCacheStats(cache, config, logContext) {
|
|
236
|
+
try {
|
|
237
|
+
const stats = await cache.getStats();
|
|
238
|
+
let output = `📊 缓存统计信息\n\n📦 总消息数: ${stats.totalMessages}\n`;
|
|
239
|
+
if (stats.oldestMessage)
|
|
240
|
+
output += `📅 最早消息: ${new Date(stats.oldestMessage).toLocaleString('zh-CN')}\n`;
|
|
241
|
+
if (stats.newestMessage)
|
|
242
|
+
output += `📅 最新消息: ${new Date(stats.newestMessage).toLocaleString('zh-CN')}\n`;
|
|
243
|
+
output += '\n📡 按订阅统计:\n';
|
|
244
|
+
Object.entries(stats.bySubscription).sort(([, a], [, b]) => b - a).slice(0, 10).forEach(([rssId, count]) => {
|
|
245
|
+
output += ` ${rssId}: ${count}条\n`;
|
|
246
|
+
});
|
|
247
|
+
output += '\n👥 按群组统计:\n';
|
|
248
|
+
Object.entries(stats.byGuild).sort(([, a], [, b]) => b - a).slice(0, 10).forEach(([guild, count]) => {
|
|
249
|
+
output += ` ${guild}: ${count}条\n`;
|
|
250
|
+
});
|
|
251
|
+
output += `\n⚙️ 最大缓存限制: ${cache.getMaxCacheSize()}条`;
|
|
252
|
+
return output;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
logCommandError(config, error, 'cache stats error', logContext);
|
|
256
|
+
return `获取统计信息失败: ${error.message}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function handleCacheClear(cache, config, logContext) {
|
|
260
|
+
try {
|
|
261
|
+
const deletedCount = await cache.clearAll();
|
|
262
|
+
return `✅ 已清空所有缓存,共删除 ${deletedCount} 条消息`;
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
logCommandError(config, error, 'cache clear error', logContext);
|
|
266
|
+
return `清空缓存失败: ${error.message}`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function handleCacheCleanup(cache, args, config, logContext) {
|
|
270
|
+
const keepLatest = parseInt(args[0]) || cache.getMaxCacheSize();
|
|
271
|
+
try {
|
|
272
|
+
const deletedCount = await cache.cleanup({ keepLatest });
|
|
273
|
+
if (deletedCount === 0) {
|
|
274
|
+
return '✅ 当前缓存数量未超过限制,无需清理';
|
|
275
|
+
}
|
|
276
|
+
return `✅ 已清理缓存,保留最新 ${keepLatest} 条,删除 ${deletedCount} 条消息`;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
logCommandError(config, error, 'cache cleanup error', { ...logContext, keepLatest });
|
|
280
|
+
return `清理缓存失败: ${error.message}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function handleCachePull(ctx, session, cache, args, config, logContext) {
|
|
284
|
+
const serialNumber = parseInt(args[0]);
|
|
285
|
+
if (!serialNumber || serialNumber < 1) {
|
|
286
|
+
return '请提供有效的序号\n使用方法: rsso.cache pull <序号>\n示例: rsso.cache pull 1\n💡 提示:使用 "rsso.cache list" 查看序号';
|
|
287
|
+
}
|
|
288
|
+
let found = null;
|
|
289
|
+
const targetContext = {
|
|
290
|
+
...logContext,
|
|
291
|
+
serialNumber,
|
|
292
|
+
};
|
|
293
|
+
try {
|
|
294
|
+
found = await findCachedMessageBySerial(cache, serialNumber);
|
|
295
|
+
if (!found.message) {
|
|
296
|
+
return `❌ 未找到序号为 ${args[0]} 的消息\n💡 使用 "rsso.cache list" 查看可用的序号`;
|
|
297
|
+
}
|
|
298
|
+
if (!found.message.finalMessage) {
|
|
299
|
+
return '❌ 该消息没有缓存的最终消息\n💡 这条消息可能是旧版本缓存,请重新订阅后重试';
|
|
300
|
+
}
|
|
301
|
+
const { id: guildId } = session.event.guild;
|
|
302
|
+
const { platform } = session.event;
|
|
303
|
+
const target = `${platform}:${guildId}`;
|
|
304
|
+
await ctx.broadcast([target], found.message.finalMessage);
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
const { id: guildId } = session.event.guild;
|
|
309
|
+
const { platform } = session.event;
|
|
310
|
+
logCommandError(config, error, 'cache pull error', {
|
|
311
|
+
...targetContext,
|
|
312
|
+
guildId,
|
|
313
|
+
platform,
|
|
314
|
+
target: `${platform}:${guildId}`,
|
|
315
|
+
cachedMessageId: found?.message?.id,
|
|
316
|
+
rssId: found?.message?.rssId,
|
|
317
|
+
});
|
|
318
|
+
return `推送消息失败: ${error.message}`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function handleQueueStats(queueManager, config, logContext) {
|
|
322
|
+
try {
|
|
323
|
+
const stats = await queueManager.getStats();
|
|
324
|
+
const total = stats.pending + stats.retry + stats.failed + stats.success;
|
|
325
|
+
return `📊 发送队列统计\n\n⏳ 待发送: ${stats.pending}\n🔄 等待重试: ${stats.retry}\n❌ 发送失败: ${stats.failed}\n✅ 发送成功: ${stats.success}\n\n📦 总计: ${total} 个任务`;
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
logCommandError(config, error, 'queue stats error', logContext);
|
|
329
|
+
return `获取统计信息失败: ${error.message}`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function handleQueueRetry(queueManager, args, config, logContext) {
|
|
333
|
+
try {
|
|
334
|
+
const taskId = args[0];
|
|
335
|
+
if (taskId === '--all') {
|
|
336
|
+
const count = await queueManager.retryFailedTasks();
|
|
337
|
+
return `✅ 已重置 ${count} 个失败任务为 PENDING 状态`;
|
|
338
|
+
}
|
|
339
|
+
if (taskId) {
|
|
340
|
+
const id = parseInt(taskId);
|
|
341
|
+
if (isNaN(id)) {
|
|
342
|
+
return `❌ 无效的任务ID: ${taskId}`;
|
|
343
|
+
}
|
|
344
|
+
const count = await queueManager.retryFailedTasks(id);
|
|
345
|
+
return count > 0 ? `✅ 已重置任务 ${id}` : `❌ 未找到任务 ${id}`;
|
|
346
|
+
}
|
|
347
|
+
return '请指定任务ID或使用 --all 重试所有失败任务\n使用方法: rsso.queue retry <id|--all>';
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
logCommandError(config, error, 'queue retry error', { ...logContext, taskId: args[0] });
|
|
351
|
+
return `重试失败: ${error.message}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function handleQueueCleanup(queueManager, args, config, logContext) {
|
|
355
|
+
try {
|
|
356
|
+
const hours = parseInt(args[0]) || 24;
|
|
357
|
+
const count = await queueManager.cleanupSuccessTasks(hours);
|
|
358
|
+
if (count === 0) {
|
|
359
|
+
return '✅ 没有需要清理的成功任务';
|
|
360
|
+
}
|
|
361
|
+
return `✅ 已清理 ${count} 个超过 ${hours} 小时的成功任务`;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
logCommandError(config, error, 'queue cleanup error', { ...logContext, hours: parseInt(args[0]) || 24 });
|
|
365
|
+
return `清理失败: ${error.message}`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function findCachedMessageBySerial(cache, serialNumber, limit = 10, maxPagesToSearch = 10) {
|
|
369
|
+
let targetSerialNumber = serialNumber;
|
|
370
|
+
for (let page = 1; page <= maxPagesToSearch; page++) {
|
|
371
|
+
const offset = (page - 1) * limit;
|
|
372
|
+
const messages = await cache.getMessages({ limit, offset });
|
|
373
|
+
if (messages.length === 0)
|
|
374
|
+
break;
|
|
375
|
+
if (targetSerialNumber <= messages.length) {
|
|
376
|
+
return { message: messages[targetSerialNumber - 1], page };
|
|
377
|
+
}
|
|
378
|
+
targetSerialNumber -= messages.length;
|
|
379
|
+
}
|
|
380
|
+
return { message: null, page: 1 };
|
|
381
|
+
}
|
|
382
|
+
function logCommandError(config, error, scope, context) {
|
|
383
|
+
const normalizedError = (0, error_handler_1.normalizeError)(error);
|
|
384
|
+
(0, logger_1.debug)(config, normalizedError, scope, 'error', context);
|
|
385
|
+
(0, error_tracker_1.trackError)(normalizedError, context);
|
|
386
|
+
}
|
|
387
|
+
var subscription_management_1 = require("./subscription-management");
|
|
388
|
+
Object.defineProperty(exports, "registerSubscriptionManagementCommands", { enumerable: true, get: function () { return subscription_management_1.registerSubscriptionManagementCommands; } });
|
|
389
|
+
var subscription_edit_1 = require("./subscription-edit");
|
|
390
|
+
Object.defineProperty(exports, "registerSubscriptionEditCommand", { enumerable: true, get: function () { return subscription_edit_1.registerSubscriptionEditCommand; } });
|
|
391
|
+
var subscription_create_1 = require("./subscription-create");
|
|
392
|
+
Object.defineProperty(exports, "registerSubscriptionCreateCommand", { enumerable: true, get: function () { return subscription_create_1.registerSubscriptionCreateCommand; } });
|
|
393
|
+
var web_monitor_1 = require("./web-monitor");
|
|
394
|
+
Object.defineProperty(exports, "registerWebMonitorCommands", { enumerable: true, get: function () { return web_monitor_1.registerWebMonitorCommands; } });
|
|
395
|
+
var runtime_1 = require("./runtime");
|
|
396
|
+
Object.defineProperty(exports, "createCommandRuntimeDeps", { enumerable: true, get: function () { return runtime_1.createCommandRuntimeDeps; } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import type { Config, rssArg } from '../types';
|
|
3
|
+
import { RssItemProcessor } from '../core/item-processor';
|
|
4
|
+
type DebugType = 'disable' | 'error' | 'info' | 'details';
|
|
5
|
+
export interface CommandRuntimeDeps {
|
|
6
|
+
debug: (message: any, name?: string, type?: DebugType, context?: Record<string, any>) => void;
|
|
7
|
+
parseQuickUrl: (url: string) => string;
|
|
8
|
+
parsePubDate: (pubDate: any) => Date;
|
|
9
|
+
getRssData: (url: string, arg: Record<string, any>) => Promise<any[]>;
|
|
10
|
+
parseRssItem: (item: any, arg: Record<string, any>, authorId: string | number) => Promise<string>;
|
|
11
|
+
formatArg: (options: Record<string, any>) => rssArg;
|
|
12
|
+
mixinArg: (arg: Record<string, any>) => rssArg;
|
|
13
|
+
generateSelectorByAI: (url: string, instruction: string, html: string) => Promise<string>;
|
|
14
|
+
fetchUrl: (url: string, arg?: Record<string, any>) => Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createCommandRuntimeDeps(ctx: Context, config: Config, $http: any, processor: RssItemProcessor): CommandRuntimeDeps;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCommandRuntimeDeps = createCommandRuntimeDeps;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
const ai_1 = require("../core/ai");
|
|
6
|
+
const feeder_1 = require("../core/feeder");
|
|
7
|
+
const parser_1 = require("../core/parser");
|
|
8
|
+
const common_1 = require("../utils/common");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
function createCommandRuntimeDeps(ctx, config, $http, processor) {
|
|
11
|
+
return {
|
|
12
|
+
debug: (message, name = '', type = 'details', context) => {
|
|
13
|
+
(0, logger_1.debug)(config, message, name, type, context);
|
|
14
|
+
},
|
|
15
|
+
parseQuickUrl: (url) => (0, common_1.parseQuickUrl)(url, config.msg?.rssHubUrl, constants_1.quickList),
|
|
16
|
+
parsePubDate: (pubDate) => (0, common_1.parsePubDate)(config, pubDate),
|
|
17
|
+
getRssData: (url, arg) => (0, parser_1.getRssData)(ctx, config, $http, url, arg),
|
|
18
|
+
parseRssItem: (item, arg, authorId) => processor.parseRssItem(item, arg, authorId),
|
|
19
|
+
formatArg: (options) => (0, feeder_1.formatArg)(options, config),
|
|
20
|
+
mixinArg: (arg) => (0, feeder_1.mixinArg)(arg, config),
|
|
21
|
+
generateSelectorByAI: (url, instruction, html) => (0, ai_1.generateSelectorByAI)(config, url, instruction, html),
|
|
22
|
+
fetchUrl: async (url, arg = {}) => {
|
|
23
|
+
const response = await $http(url, arg);
|
|
24
|
+
return String(response?.data ?? '');
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import type { Config } from '../types';
|
|
3
|
+
export interface SubscriptionCreateCommandDeps {
|
|
4
|
+
ctx: Context;
|
|
5
|
+
config: Config;
|
|
6
|
+
usage: string;
|
|
7
|
+
quickList: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
detail: string;
|
|
10
|
+
example: string;
|
|
11
|
+
}>;
|
|
12
|
+
parseQuickUrl: (url: string) => string;
|
|
13
|
+
parsePubDate: (pubDate: any) => Date;
|
|
14
|
+
getRssData: (url: string, arg: any) => Promise<any[]>;
|
|
15
|
+
parseRssItem: (item: any, arg: any, authorId: string | number) => Promise<string>;
|
|
16
|
+
formatArg: (options: Record<string, any>) => any;
|
|
17
|
+
mixinArg: (arg: any) => any;
|
|
18
|
+
debug: (message: any, name?: string, type?: 'disable' | 'error' | 'info' | 'details', context?: Record<string, any>) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 注册主订阅命令。
|
|
22
|
+
*/
|
|
23
|
+
export declare function registerSubscriptionCreateCommand(deps: SubscriptionCreateCommandDeps): void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSubscriptionCreateCommand = registerSubscriptionCreateCommand;
|
|
4
|
+
const common_1 = require("../utils/common");
|
|
5
|
+
const error_handler_1 = require("../utils/error-handler");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
/**
|
|
8
|
+
* 注册主订阅命令。
|
|
9
|
+
*/
|
|
10
|
+
function registerSubscriptionCreateCommand(deps) {
|
|
11
|
+
deps.ctx.guild()
|
|
12
|
+
.command('rssowl <url:text>', '订阅 RSS/源')
|
|
13
|
+
.alias('rsso')
|
|
14
|
+
.usage(deps.usage)
|
|
15
|
+
.option('list', '-l [content] 查看订阅列表(详情) [已移至 rsso.list 子命令,使用列表序号]')
|
|
16
|
+
.option('remove', '-r <序号> 删除订阅 [已移至 rsso.remove 子命令,使用列表序号]')
|
|
17
|
+
.option('removeAll', '删除全部订阅 [已移至 rsso.remove --all 子命令]')
|
|
18
|
+
.option('follow', '-f <序号> 关注订阅 [已移至 rsso.follow 子命令,使用列表序号]')
|
|
19
|
+
.option('followAll', '<序号> 在该订阅更新时提醒所有人 [已移至 rsso.follow --all 子命令,使用列表序号]')
|
|
20
|
+
.option('target', '--target <platform:guildId> 跨群订阅(高级权限)')
|
|
21
|
+
.option('arg', '-a <content> 自定义配置')
|
|
22
|
+
.option('template', '-i <content> 消息模板')
|
|
23
|
+
.option('title', '-t <content> 自定义命名')
|
|
24
|
+
.option('pull', '-p <序号> 拉取订阅最新更新 [已移至 rsso.pull 子命令,使用列表序号]')
|
|
25
|
+
.option('force', '强行写入')
|
|
26
|
+
.option('daily', '-d <content>')
|
|
27
|
+
.option('test', '-T 测试')
|
|
28
|
+
.option('quick', '-q [content] 查询快速订阅列表')
|
|
29
|
+
.example('rsso https://hub.slarker.me/qqorw')
|
|
30
|
+
.action(async ({ session, options }, url) => {
|
|
31
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso', 'create');
|
|
32
|
+
deps.debug(options, 'options', 'info', logContext);
|
|
33
|
+
const { guildId, platform, authorId: userId, authority } = (0, utils_1.extractSessionInfo)(session);
|
|
34
|
+
const botSelfId = session.bot?.selfId;
|
|
35
|
+
deps.debug(`${platform}:${userId}:${guildId}, bot:${botSelfId}`, '', 'info', logContext);
|
|
36
|
+
if (options?.quick === '') {
|
|
37
|
+
return '输入 rsso -q [id] 查询详情\n' + deps.quickList.map((v, i) => `${i + 1}.${v.name}`).join('\n');
|
|
38
|
+
}
|
|
39
|
+
if (options?.quick) {
|
|
40
|
+
const currentQuickObj = deps.quickList[parseInt(options.quick) - 1];
|
|
41
|
+
if (!currentQuickObj)
|
|
42
|
+
return `快速订阅编号不存在: ${options.quick}`;
|
|
43
|
+
return `${currentQuickObj.name}\n${currentQuickObj.detail}\n例:rsso -T ${currentQuickObj.example}\n(${deps.parseQuickUrl(currentQuickObj.example)})`;
|
|
44
|
+
}
|
|
45
|
+
if (platform.includes('sandbox') && !options.test && url) {
|
|
46
|
+
session.send('沙盒中无法推送更新,但RSS依然会被订阅,建议使用 -T 选项进行测试');
|
|
47
|
+
}
|
|
48
|
+
const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
|
|
49
|
+
if (options?.list === '' || options?.list)
|
|
50
|
+
return '💡 提示:请使用子命令查看订阅\n\nrsso.list - 查看所有订阅(显示序号)\nrsso.list 1 - 查看订阅详情\n\n(旧选项 -l 仍可使用,但建议迁移到新命令)';
|
|
51
|
+
if (options?.remove)
|
|
52
|
+
return '💡 提示:请使用子命令删除订阅\n\nrsso.remove 1 - 删除订阅 #1(使用列表序号)\nrsso.remove --all - 删除全部订阅\n\n(旧选项 -r 仍可使用,但建议迁移到新命令)';
|
|
53
|
+
if (options?.removeAll)
|
|
54
|
+
return '💡 提示:请使用子命令删除订阅\n\nrsso.remove --all - 删除全部订阅\n\n(旧选项仍可使用,但建议迁移到新命令)';
|
|
55
|
+
if (options?.follow)
|
|
56
|
+
return '💡 提示:请使用子命令关注订阅\n\nrsso.follow 1 - 关注订阅 #1(使用列表序号)\n\n(旧选项 -f 仍可使用,但建议迁移到新命令)';
|
|
57
|
+
if (options?.followAll)
|
|
58
|
+
return '💡 提示:请使用子命令设置全员提醒\n\nrsso.follow 1 --all - 设置全员提醒(使用列表序号)\n\n(旧选项仍可使用,但建议迁移到新命令)';
|
|
59
|
+
if (options?.pull)
|
|
60
|
+
return '💡 提示:请使用子命令拉取订阅\n\nrsso.pull 1 - 拉取订阅 #1 的最新更新(使用列表序号)\n\n(旧选项 -p 仍可使用,但建议迁移到新命令)';
|
|
61
|
+
if (!url)
|
|
62
|
+
return deps.usage;
|
|
63
|
+
if (rssList.find(item => item.url === url))
|
|
64
|
+
return '该订阅已存在';
|
|
65
|
+
const rawArg = deps.formatArg(options);
|
|
66
|
+
const arg = deps.mixinArg(rawArg);
|
|
67
|
+
let targetPlatform = platform;
|
|
68
|
+
let targetGuildId = guildId;
|
|
69
|
+
if (options?.target) {
|
|
70
|
+
const authorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.advancedAuthority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.advancedAuthority} 或以上`);
|
|
71
|
+
if (!authorityCheck.success)
|
|
72
|
+
return authorityCheck.message;
|
|
73
|
+
const parsedTarget = (0, utils_1.parseTarget)(options.target);
|
|
74
|
+
if (!parsedTarget) {
|
|
75
|
+
return '请输入正确的群号,格式为 platform:guildId 或 platform:guildId\n示例: onebot:123456';
|
|
76
|
+
}
|
|
77
|
+
targetPlatform = parsedTarget.platform;
|
|
78
|
+
targetGuildId = parsedTarget.guildId;
|
|
79
|
+
if (options.test) {
|
|
80
|
+
try {
|
|
81
|
+
await deps.ctx.broadcast([`${targetPlatform}:${targetGuildId}`], '📤 跨群订阅测试消息');
|
|
82
|
+
return `✅ 测试消息已发送到目标群组\n目标: ${targetPlatform}:${targetGuildId}\n\n说明:Bot 可以访问该群组,跨群订阅可以正常工作。\n去掉 --test 选项完成订阅。`;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return `❌ 无法发送到目标群组\n目标: ${targetPlatform}:${targetGuildId}\n错误: ${error.message}\n\n请确认:\n1. Bot 是否在该群组中\n2. 群组ID 是否正确\n3. 平台名称是否正确(如 onebot, telegram 等)`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let title = options?.title || '';
|
|
90
|
+
try {
|
|
91
|
+
url = deps.parseQuickUrl(url);
|
|
92
|
+
const rssItemList = await deps.getRssData((0, common_1.ensureUrlProtocol)(url), arg);
|
|
93
|
+
if (options.test) {
|
|
94
|
+
const testItem = rssItemList[0];
|
|
95
|
+
if (!testItem)
|
|
96
|
+
return '未获取到数据';
|
|
97
|
+
const testArg = { ...arg, url: title || testItem.rss.channel.title, title: title || testItem.rss.channel.title };
|
|
98
|
+
if (!testArg.template)
|
|
99
|
+
testArg.template = deps.config.basic.defaultTemplate;
|
|
100
|
+
return deps.parseRssItem(testItem, testArg, userId);
|
|
101
|
+
}
|
|
102
|
+
if (!title) {
|
|
103
|
+
title = rssItemList[0]?.rss.channel.title;
|
|
104
|
+
if (!title)
|
|
105
|
+
return '无法获取标题,请使用 -t 指定标题';
|
|
106
|
+
}
|
|
107
|
+
const lastPubDate = deps.parsePubDate(rssItemList[0]?.pubDate);
|
|
108
|
+
const rssItem = {
|
|
109
|
+
url,
|
|
110
|
+
platform: targetPlatform,
|
|
111
|
+
guildId: targetGuildId,
|
|
112
|
+
author: botSelfId,
|
|
113
|
+
rssId: rssItemList[0]?.rss?.channel?.title ? rssItemList[0].rss.channel.title : title,
|
|
114
|
+
arg: rawArg,
|
|
115
|
+
title,
|
|
116
|
+
lastPubDate,
|
|
117
|
+
lastContent: [],
|
|
118
|
+
followers: []
|
|
119
|
+
};
|
|
120
|
+
if (options.force) {
|
|
121
|
+
const forceCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.authority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.authority} 或以上`);
|
|
122
|
+
if (!forceCheck.success)
|
|
123
|
+
return forceCheck.message;
|
|
124
|
+
}
|
|
125
|
+
else if (deps.config.basic.urlDeduplication && rssList.find(item => item.rssId === rssItem.rssId)) {
|
|
126
|
+
return `订阅已存在: ${rssItem.rssId}`;
|
|
127
|
+
}
|
|
128
|
+
await deps.ctx.database.create('rssOwl', rssItem);
|
|
129
|
+
if (deps.config.basic.firstLoad && arg.firstLoad !== false && rssItemList.length > 0) {
|
|
130
|
+
let itemArray = rssItemList.sort((a, b) => deps.parsePubDate(b.pubDate).getTime() - deps.parsePubDate(a.pubDate).getTime());
|
|
131
|
+
if (arg.reverse)
|
|
132
|
+
itemArray = itemArray.reverse();
|
|
133
|
+
const maxItem = arg.forceLength || 1;
|
|
134
|
+
const mergedArg = deps.mixinArg(rssItem.arg);
|
|
135
|
+
const messageList = await Promise.all(itemArray.filter((_, index) => index < maxItem).map(async (item) => deps.parseRssItem(item, { ...rssItem, ...mergedArg }, rssItem.author)));
|
|
136
|
+
await deps.ctx.broadcast([`${targetPlatform}:${targetGuildId}`], messageList.join(''));
|
|
137
|
+
}
|
|
138
|
+
return `订阅成功: ${title}`;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
deps.debug(error, 'add error', 'error', logContext);
|
|
142
|
+
return `订阅失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, '添加订阅')}`;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|