@anyul/koishi-plugin-rss 5.2.2 → 5.2.3

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 (38) hide show
  1. package/lib/commands/error-handler.js +2 -5
  2. package/lib/commands/index.d.ts +17 -1
  3. package/lib/commands/index.js +388 -2
  4. package/lib/commands/subscription-edit.d.ts +7 -0
  5. package/lib/commands/subscription-edit.js +177 -0
  6. package/lib/commands/subscription-management.d.ts +12 -0
  7. package/lib/commands/subscription-management.js +176 -0
  8. package/lib/commands/utils.d.ts +13 -1
  9. package/lib/commands/utils.js +43 -2
  10. package/lib/config.js +19 -0
  11. package/lib/core/ai.d.ts +16 -2
  12. package/lib/core/ai.js +73 -6
  13. package/lib/core/feeder.d.ts +1 -1
  14. package/lib/core/feeder.js +238 -125
  15. package/lib/core/item-processor.d.ts +5 -0
  16. package/lib/core/item-processor.js +66 -136
  17. package/lib/core/notification-queue.d.ts +2 -0
  18. package/lib/core/notification-queue.js +80 -33
  19. package/lib/core/parser.js +12 -0
  20. package/lib/core/renderer.d.ts +15 -0
  21. package/lib/core/renderer.js +91 -23
  22. package/lib/index.js +28 -784
  23. package/lib/tsconfig.tsbuildinfo +1 -1
  24. package/lib/types.d.ts +24 -0
  25. package/lib/utils/common.js +52 -3
  26. package/lib/utils/error-handler.d.ts +8 -0
  27. package/lib/utils/error-handler.js +27 -0
  28. package/lib/utils/error-tracker.js +24 -8
  29. package/lib/utils/fetcher.js +68 -9
  30. package/lib/utils/logger.d.ts +4 -2
  31. package/lib/utils/logger.js +144 -6
  32. package/lib/utils/media.js +3 -6
  33. package/lib/utils/sanitizer.d.ts +58 -0
  34. package/lib/utils/sanitizer.js +227 -0
  35. package/lib/utils/security.d.ts +75 -0
  36. package/lib/utils/security.js +312 -0
  37. package/lib/utils/structured-logger.js +3 -20
  38. package/package.json +2 -1
@@ -10,9 +10,8 @@ exports.permissionDenied = permissionDenied;
10
10
  exports.invalidArgument = invalidArgument;
11
11
  exports.notFound = notFound;
12
12
  exports.alreadyExists = alreadyExists;
13
- const koishi_1 = require("koishi");
14
13
  const error_handler_1 = require("../utils/error-handler");
15
- const logger = new koishi_1.Logger('rss-owl-command');
14
+ const logger_1 = require("../utils/logger");
16
15
  /**
17
16
  * 命令错误类型
18
17
  */
@@ -62,9 +61,7 @@ async function executeCommand(ctx, config, operationName, handler) {
62
61
  * 记录命令错误
63
62
  */
64
63
  function logError(config, operation, error) {
65
- if (config.debug === 'details' || config.debug === 'error') {
66
- logger.error(`[${operation}]`, error);
67
- }
64
+ (0, logger_1.debugError)(config, error, operation);
68
65
  }
69
66
  /**
70
67
  * 格式化命令错误消息
@@ -1 +1,17 @@
1
- export {};
1
+ import { Context } from 'koishi';
2
+ import type { Config } from '../types';
3
+ import { NotificationQueueManager } from '../core/notification-queue';
4
+ /**
5
+ * 管理类命令依赖
6
+ */
7
+ export interface ManagementCommandDeps {
8
+ ctx: Context;
9
+ config: Config;
10
+ queueManager: NotificationQueueManager;
11
+ }
12
+ /**
13
+ * 注册管理类命令
14
+ */
15
+ export declare function registerManagementCommands(deps: ManagementCommandDeps): void;
16
+ export { registerSubscriptionManagementCommands } from './subscription-management';
17
+ export { registerSubscriptionEditCommand } from './subscription-edit';
@@ -1,4 +1,390 @@
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.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; } });
@@ -0,0 +1,7 @@
1
+ import { Context } from 'koishi';
2
+ import type { Config } from '../types';
3
+ export interface SubscriptionEditCommandDeps {
4
+ ctx: Context;
5
+ config: Config;
6
+ }
7
+ export declare function registerSubscriptionEditCommand(deps: SubscriptionEditCommandDeps): void;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSubscriptionEditCommand = registerSubscriptionEditCommand;
4
+ const utils_1 = require("./utils");
5
+ function registerSubscriptionEditCommand(deps) {
6
+ deps.ctx.guild()
7
+ .command('rssowl.edit <id:number>', '修改订阅配置')
8
+ .alias('rsso.edit')
9
+ .usage(`修改订阅的配置项
10
+
11
+ 用法:
12
+ rsso.edit 1 -t "新标题" - 修改标题
13
+ rsso.edit 1 -i content - 修改模板
14
+ rsso.edit 1 -u https://... - 修改URL
15
+ rsso.edit 1 -s ".item" - 修改选择器(HTML监控)
16
+ rsso.edit 1 --target onebot:123 - 修改推送目标(高级权限)
17
+ rsso.edit 1 --target "onebot:123,telegram:456" - 多个推送目标(高级权限)⭐
18
+ rsso.edit 1 -t "新标题" --test - 测试修改(不保存)
19
+
20
+ 示例:
21
+ rsso.edit 1 -t "我的订阅"
22
+ rsso.edit 1 -i custom
23
+ rsso.edit 1 --target onebot:123456
24
+ rsso.edit 1 --target "onebot:123,telegram:456"
25
+
26
+ 💡 提示:
27
+ - 使用列表序号(1, 2, 3...)而不是数据库ID
28
+ - 推送目标格式: platform:guildId
29
+ - 多个目标用逗号分隔或多次使用 --target
30
+ `)
31
+ .option('title', '-t <title> 修改标题', { type: 'string' })
32
+ .option('url', '-u <url> 修改URL', { type: 'string' })
33
+ .option('template', '-i <template> 修改模板', { type: 'string' })
34
+ .option('selector', '-s <selector> 修改选择器(HTML监控)', { type: 'string' })
35
+ .option('target', '--target <target> 修改推送目标(高级权限)')
36
+ .option('test', '--test 测试修改(不保存)')
37
+ .example('rsso.edit 1 -t "新标题"')
38
+ .action(async ({ session, options }, id) => {
39
+ const { guildId, platform, authority } = (0, utils_1.extractSessionInfo)(session);
40
+ const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
41
+ const listIndex = id - 1;
42
+ if (listIndex < 0 || listIndex >= rssList.length) {
43
+ return `❌ 序号 ${id} 不存在\n当前共有 ${rssList.length} 个订阅\n\n使用 rsso.list 查看完整列表`;
44
+ }
45
+ const rssItem = rssList[listIndex];
46
+ const baseAuthorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.authority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.authority} 或以上`);
47
+ if (!baseAuthorityCheck.success)
48
+ return baseAuthorityCheck.message;
49
+ if (options.target) {
50
+ const targetAuthorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.advancedAuthority, `❌ 修改推送目标需要高级权限\n当前权限: ${authority},需要权限: ${deps.config.basic.advancedAuthority} 或以上`);
51
+ if (!targetAuthorityCheck.success)
52
+ return targetAuthorityCheck.message;
53
+ }
54
+ const hasChanges = options.title || options.url || options.template || options.selector || options.target;
55
+ if (!hasChanges) {
56
+ return '请指定要修改的内容\n可用选项: -t (标题), -u (URL), -i (模板), -s (选择器), --target (推送目标)\n使用 --help 查看详细帮助';
57
+ }
58
+ const parseResult = (0, utils_1.parseTargets)(options.target);
59
+ if (parseResult.invalidTarget) {
60
+ return `❌ 推送目标格式错误: "${parseResult.invalidTarget}"\n正确格式: platform:guildId\n示例: onebot:123456\n\n支持多个目标,用逗号分隔:\n --target "onebot:123,telegram:456"`;
61
+ }
62
+ if (options.test) {
63
+ return buildEditPreview(rssItem, id, options, parseResult.targets);
64
+ }
65
+ return saveSubscriptionChanges(deps.ctx, rssItem, id, options, parseResult.targets);
66
+ });
67
+ }
68
+ function buildEditPreview(rssItem, id, options, parsedTargets) {
69
+ let testOutput = `📝 修改预览 [序号:${id} | ID:${rssItem.id}]\n\n`;
70
+ testOutput += `当前标题: ${rssItem.title}\n`;
71
+ testOutput += `当前URL: ${rssItem.url}\n`;
72
+ testOutput += `当前推送目标: ${rssItem.platform}:${rssItem.guildId}\n`;
73
+ if (options.title)
74
+ testOutput += `→ 新标题: ${options.title}\n`;
75
+ if (options.url)
76
+ testOutput += `→ 新URL: ${options.url}\n`;
77
+ if (options.template)
78
+ testOutput += `→ 新模板: ${options.template}\n`;
79
+ if (options.selector)
80
+ testOutput += `→ 新选择器: ${options.selector}\n`;
81
+ if (parsedTargets.length > 0) {
82
+ if (parsedTargets.length === 1) {
83
+ testOutput += `→ 新推送目标: ${parsedTargets[0]}\n`;
84
+ }
85
+ else {
86
+ testOutput += `→ 新推送目标:\n ${parsedTargets.join('\n ')}\n`;
87
+ testOutput += '\n⚠️ 注意: 多个推送目标会创建多个订阅记录\n每个目标会复制当前的订阅配置\n';
88
+ }
89
+ }
90
+ testOutput += '\n⚠️ 测试模式:不会保存修改\n去掉 --test 选项保存更改';
91
+ return testOutput;
92
+ }
93
+ async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets) {
94
+ const updates = {};
95
+ if (options.title)
96
+ updates.title = options.title;
97
+ if (options.url)
98
+ updates.url = options.url;
99
+ if (options.template) {
100
+ if (!rssItem.arg)
101
+ rssItem.arg = {};
102
+ rssItem.arg.template = options.template;
103
+ updates.arg = rssItem.arg;
104
+ }
105
+ if (options.selector) {
106
+ if (!rssItem.arg)
107
+ rssItem.arg = {};
108
+ rssItem.arg.selector = options.selector;
109
+ updates.arg = rssItem.arg;
110
+ }
111
+ try {
112
+ if (parsedTargets.length > 0) {
113
+ if (parsedTargets.length === 1) {
114
+ const [newPlatform, newGuildId] = parsedTargets[0].split(/[::]/);
115
+ updates.platform = newPlatform;
116
+ updates.guildId = newGuildId;
117
+ await ctx.database.set('rssOwl', rssItem.id, updates);
118
+ let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
119
+ if (options.title)
120
+ result += `标题: ${rssItem.title} → ${options.title}\n`;
121
+ if (options.url)
122
+ result += `URL: ${rssItem.url} → ${options.url}\n`;
123
+ if (options.template)
124
+ result += `模板: ${rssItem.arg?.template || 'default'} → ${options.template}\n`;
125
+ if (options.selector)
126
+ result += `选择器: ${rssItem.arg?.selector || '无'} → ${options.selector}\n`;
127
+ result += `推送目标: ${rssItem.platform}:${rssItem.guildId} → ${parsedTargets[0]}\n`;
128
+ return result.trim();
129
+ }
130
+ const originalTarget = `${rssItem.platform}:${rssItem.guildId}`;
131
+ let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
132
+ result += `已创建 ${parsedTargets.length} 个推送目标:\n\n`;
133
+ result += `1️⃣ 原订阅 (本群): ${originalTarget}\n`;
134
+ await ctx.database.set('rssOwl', rssItem.id, updates);
135
+ for (let i = 0; i < parsedTargets.length; i++) {
136
+ const [newPlatform, newGuildId] = parsedTargets[i].split(/[::]/);
137
+ const existing = await ctx.database.get('rssOwl', {
138
+ platform: newPlatform,
139
+ guildId: newGuildId,
140
+ url: rssItem.url
141
+ });
142
+ if (existing.length > 0) {
143
+ result += `${i + 2}. ⚠️ ${newPlatform}:${newGuildId} (订阅已存在,已跳过)\n`;
144
+ continue;
145
+ }
146
+ const newSubscription = {
147
+ ...rssItem,
148
+ id: undefined,
149
+ platform: newPlatform,
150
+ guildId: newGuildId,
151
+ };
152
+ const created = await ctx.database.create('rssOwl', newSubscription);
153
+ result += `${i + 2}. ✅ ${newPlatform}:${newGuildId} (新订阅ID: ${created.id})\n`;
154
+ }
155
+ if (options.title)
156
+ result += `\n标题: ${rssItem.title} → ${options.title}\n`;
157
+ if (options.url)
158
+ result += `URL: ${rssItem.url} → ${options.url}\n`;
159
+ result += '\n💡 提示: 可以使用 rsso.list 查看所有订阅';
160
+ return result.trim();
161
+ }
162
+ await ctx.database.set('rssOwl', rssItem.id, updates);
163
+ let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
164
+ if (options.title)
165
+ result += `标题: ${rssItem.title} → ${options.title}\n`;
166
+ if (options.url)
167
+ result += `URL: ${rssItem.url} → ${options.url}\n`;
168
+ if (options.template)
169
+ result += `模板: ${rssItem.arg?.template || 'default'} → ${options.template}\n`;
170
+ if (options.selector)
171
+ result += `选择器: ${rssItem.arg?.selector || '无'} → ${options.selector}\n`;
172
+ return result.trim();
173
+ }
174
+ catch (error) {
175
+ return `更新失败: ${error.message}`;
176
+ }
177
+ }
@@ -0,0 +1,12 @@
1
+ import { Context } from 'koishi';
2
+ import type { Config } from '../types';
3
+ export interface SubscriptionCommandDeps {
4
+ ctx: Context;
5
+ config: Config;
6
+ parsePubDate: (pubDate: any) => Date;
7
+ parseQuickUrl: (url: string) => string;
8
+ getRssData: (url: string, arg: any) => Promise<any[]>;
9
+ parseRssItem: (item: any, arg: any, authorId: string | number) => Promise<string>;
10
+ mixinArg: (arg: any) => any;
11
+ }
12
+ export declare function registerSubscriptionManagementCommands(deps: SubscriptionCommandDeps): void;