@anyul/koishi-plugin-rss 5.3.2 → 5.3.10

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 (59) hide show
  1. package/lib/commands/index.js +1 -3
  2. package/lib/commands/subscription-create.d.ts +2 -0
  3. package/lib/commands/subscription-create.js +2 -2
  4. package/lib/commands/subscription-edit.d.ts +2 -0
  5. package/lib/commands/subscription-edit.js +8 -12
  6. package/lib/commands/subscription-management.d.ts +2 -0
  7. package/lib/commands/subscription-management.js +10 -12
  8. package/lib/commands/utils.d.ts +0 -5
  9. package/lib/commands/utils.js +0 -16
  10. package/lib/commands/web-monitor.d.ts +2 -0
  11. package/lib/commands/web-monitor.js +2 -2
  12. package/lib/config.js +4 -10
  13. package/lib/core/feeder-arg.js +3 -38
  14. package/lib/core/feeder-runtime.js +0 -5
  15. package/lib/core/feeder.js +53 -12
  16. package/lib/core/item-processor-runtime.d.ts +4 -2
  17. package/lib/core/item-processor-runtime.js +4 -12
  18. package/lib/core/notification-queue-sender.js +0 -5
  19. package/lib/core/notification-queue-store.d.ts +7 -0
  20. package/lib/core/notification-queue-store.js +28 -6
  21. package/lib/core/notification-queue.d.ts +10 -0
  22. package/lib/core/notification-queue.js +18 -7
  23. package/lib/core/parser.js +5 -5
  24. package/lib/core/renderer.js +7 -7
  25. package/lib/core/search-format.d.ts +1 -1
  26. package/lib/core/search-format.js +1 -1
  27. package/lib/core/subscription-store.d.ts +44 -0
  28. package/lib/core/subscription-store.js +60 -0
  29. package/lib/core/telegram-video-restore.js +3 -8
  30. package/lib/index.d.ts +1 -4
  31. package/lib/index.js +20 -15
  32. package/lib/tsconfig.tsbuildinfo +1 -1
  33. package/lib/types.d.ts +51 -32
  34. package/lib/utils/common.d.ts +6 -0
  35. package/lib/utils/common.js +20 -30
  36. package/lib/utils/error-handler.d.ts +49 -0
  37. package/lib/utils/error-handler.js +111 -1
  38. package/lib/utils/fetcher.js +12 -38
  39. package/lib/utils/legacy-config.js +9 -3
  40. package/lib/utils/logger.d.ts +6 -0
  41. package/lib/utils/logger.js +82 -14
  42. package/lib/utils/media.js +21 -21
  43. package/lib/utils/message-cache.d.ts +8 -0
  44. package/lib/utils/message-cache.js +12 -0
  45. package/lib/utils/proxy.d.ts +37 -0
  46. package/lib/utils/proxy.js +60 -0
  47. package/package.json +16 -3
  48. package/lib/commands/error-handler.d.ts +0 -53
  49. package/lib/commands/error-handler.js +0 -120
  50. package/lib/commands/rss-subscribe.d.ts +0 -11
  51. package/lib/commands/rss-subscribe.js +0 -323
  52. package/lib/core/feeder-old.d.ts +0 -15
  53. package/lib/core/feeder-old.js +0 -403
  54. package/lib/utils/error-tracker.d.ts +0 -127
  55. package/lib/utils/error-tracker.js +0 -352
  56. package/lib/utils/metrics.d.ts +0 -184
  57. package/lib/utils/metrics.js +0 -401
  58. package/lib/utils/structured-logger.d.ts +0 -135
  59. package/lib/utils/structured-logger.js +0 -248
@@ -11,7 +11,6 @@ exports.handleCacheClear = handleCacheClear;
11
11
  exports.handleCacheCleanup = handleCacheCleanup;
12
12
  exports.handleCachePull = handleCachePull;
13
13
  const error_handler_1 = require("../utils/error-handler");
14
- const error_tracker_1 = require("../utils/error-tracker");
15
14
  const logger_1 = require("../utils/logger");
16
15
  const message_cache_1 = require("../utils/message-cache");
17
16
  const utils_1 = require("./utils");
@@ -288,7 +287,7 @@ async function handleCacheList(cmdCtx, args, config) {
288
287
  }
289
288
  }
290
289
  async function handleCacheMessage(cmdCtx, args, config) {
291
- const { cache, logContext } = cmdCtx;
290
+ const { logContext } = cmdCtx;
292
291
  const realId = parseInt(args[0]);
293
292
  if (!realId || realId < 1) {
294
293
  return '请提供真实 ID\n用法: rsso.cache message <ID>\n示例: rsso.cache message 42\n💡 提示:使用 "rsso.cache list" 查看方括号中的真实 ID';
@@ -525,7 +524,6 @@ async function resolveCacheMessageById(cmdCtx, realId) {
525
524
  function logCacheError(config, error, scope, context) {
526
525
  const normalizedError = (0, error_handler_1.normalizeError)(error);
527
526
  (0, logger_1.debug)(config, normalizedError, scope, 'error', context);
528
- (0, error_tracker_1.trackError)(normalizedError, context);
529
527
  }
530
528
  var subscription_management_1 = require("./subscription-management");
531
529
  Object.defineProperty(exports, "registerSubscriptionManagementCommands", { enumerable: true, get: function () { return subscription_management_1.registerSubscriptionManagementCommands; } });
@@ -1,4 +1,5 @@
1
1
  import { Context } from 'koishi';
2
+ import { SubscriptionStore } from '../core/subscription-store';
2
3
  import type { Config } from '../types';
3
4
  export interface QuickListItem {
4
5
  prefix: string;
@@ -20,6 +21,7 @@ export declare function resolveQuickItem(input: string, quickList: QuickListItem
20
21
  export interface SubscriptionCreateCommandDeps {
21
22
  ctx: Context;
22
23
  config: Config;
24
+ store: SubscriptionStore;
23
25
  usage: string;
24
26
  quickList: QuickListItem[];
25
27
  parseQuickUrl: (url: string) => string;
@@ -71,7 +71,7 @@ function registerSubscriptionCreateCommand(deps) {
71
71
  if (!(0, utils_1.isValidUrl)((0, common_1.ensureUrlProtocol)(url))) {
72
72
  return '❌ URL 格式不正确,请以 http:// 或 https:// 开头';
73
73
  }
74
- const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
74
+ const rssList = await deps.store.findByGuild(platform, guildId);
75
75
  if (rssList.find(item => item.url === url))
76
76
  return '❌ 该订阅已存在';
77
77
  const rawArg = deps.formatArg(options);
@@ -137,7 +137,7 @@ function registerSubscriptionCreateCommand(deps) {
137
137
  else if (deps.config.basic.urlDeduplication && rssList.find(item => item.rssId === rssItem.rssId)) {
138
138
  return `❌ 订阅已存在: ${rssItem.rssId}`;
139
139
  }
140
- await deps.ctx.database.create('rssOwl', rssItem);
140
+ await deps.store.create(rssItem);
141
141
  if (deps.config.basic.firstLoad && arg.firstLoad !== false && rssItemList.length > 0) {
142
142
  let itemArray = rssItemList.sort((a, b) => deps.parsePubDate(b.pubDate).getTime() - deps.parsePubDate(a.pubDate).getTime());
143
143
  if (arg.reverse)
@@ -1,7 +1,9 @@
1
1
  import { Context } from 'koishi';
2
+ import { SubscriptionStore } from '../core/subscription-store';
2
3
  import type { Config } from '../types';
3
4
  export interface SubscriptionEditCommandDeps {
4
5
  ctx: Context;
5
6
  config: Config;
7
+ store: SubscriptionStore;
6
8
  }
7
9
  export declare function registerSubscriptionEditCommand(deps: SubscriptionEditCommandDeps): void;
@@ -37,7 +37,7 @@ function registerSubscriptionEditCommand(deps) {
37
37
  .example('rsso.edit 1 -t "新标题"')
38
38
  .action(async ({ session, options }, id) => {
39
39
  const { guildId, platform, authority } = (0, utils_1.extractSessionInfo)(session);
40
- const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
40
+ const rssList = await deps.store.findByGuild(platform, guildId);
41
41
  const listIndex = id - 1;
42
42
  if (listIndex < 0 || listIndex >= rssList.length) {
43
43
  return `❌ 序号 ${id} 不存在\n当前共有 ${rssList.length} 个订阅\n\n使用 rsso.list 查看完整列表`;
@@ -62,7 +62,7 @@ function registerSubscriptionEditCommand(deps) {
62
62
  if (options.test) {
63
63
  return buildEditPreview(rssItem, id, options, parseResult.targets);
64
64
  }
65
- return saveSubscriptionChanges(deps.ctx, rssItem, id, options, parseResult.targets);
65
+ return saveSubscriptionChanges(deps.store, rssItem, id, options, parseResult.targets);
66
66
  });
67
67
  }
68
68
  function buildEditPreview(rssItem, id, options, parsedTargets) {
@@ -90,7 +90,7 @@ function buildEditPreview(rssItem, id, options, parsedTargets) {
90
90
  testOutput += '\n⚠️ 测试模式:不会保存修改\n去掉 --test 选项保存更改';
91
91
  return testOutput;
92
92
  }
93
- async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets) {
93
+ async function saveSubscriptionChanges(store, rssItem, id, options, parsedTargets) {
94
94
  const updates = {};
95
95
  if (options.title)
96
96
  updates.title = options.title;
@@ -114,7 +114,7 @@ async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets)
114
114
  const [newPlatform, newGuildId] = parsedTargets[0].split(/[::]/);
115
115
  updates.platform = newPlatform;
116
116
  updates.guildId = newGuildId;
117
- await ctx.database.set('rssOwl', rssItem.id, updates);
117
+ await store.update(rssItem.id, updates);
118
118
  let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
119
119
  if (options.title)
120
120
  result += `标题: ${rssItem.title} → ${options.title}\n`;
@@ -131,14 +131,10 @@ async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets)
131
131
  let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
132
132
  result += `已创建 ${parsedTargets.length} 个推送目标:\n\n`;
133
133
  result += `1. 原订阅 (本群): ${originalTarget}\n`;
134
- await ctx.database.set('rssOwl', rssItem.id, updates);
134
+ await store.update(rssItem.id, updates);
135
135
  for (let i = 0; i < parsedTargets.length; i++) {
136
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
- });
137
+ const existing = await store.findByGuildAndUrl(newPlatform, newGuildId, rssItem.url);
142
138
  if (existing.length > 0) {
143
139
  result += `${i + 2}. ⚠️ ${newPlatform}:${newGuildId} (订阅已存在,已跳过)\n`;
144
140
  continue;
@@ -149,7 +145,7 @@ async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets)
149
145
  platform: newPlatform,
150
146
  guildId: newGuildId,
151
147
  };
152
- const created = await ctx.database.create('rssOwl', newSubscription);
148
+ const created = await store.create(newSubscription);
153
149
  result += `${i + 2}. ✅ ${newPlatform}:${newGuildId} (新订阅ID: ${created.id})\n`;
154
150
  }
155
151
  if (options.title)
@@ -159,7 +155,7 @@ async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets)
159
155
  result += '\n💡 提示: 可以使用 rsso.list 查看所有订阅';
160
156
  return result.trim();
161
157
  }
162
- await ctx.database.set('rssOwl', rssItem.id, updates);
158
+ await store.update(rssItem.id, updates);
163
159
  let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
164
160
  if (options.title)
165
161
  result += `标题: ${rssItem.title} → ${options.title}\n`;
@@ -1,8 +1,10 @@
1
1
  import { Context } from 'koishi';
2
+ import { SubscriptionStore } from '../core/subscription-store';
2
3
  import type { Config } from '../types';
3
4
  export interface SubscriptionCommandDeps {
4
5
  ctx: Context;
5
6
  config: Config;
7
+ store: SubscriptionStore;
6
8
  parsePubDate: (pubDate: any) => Date;
7
9
  parseQuickUrl: (url: string) => string;
8
10
  getRssData: (url: string, arg: any) => Promise<any[]>;
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerSubscriptionManagementCommands = registerSubscriptionManagementCommands;
4
4
  const error_handler_1 = require("../utils/error-handler");
5
- const error_tracker_1 = require("../utils/error-tracker");
6
5
  const logger_1 = require("../utils/logger");
7
6
  const utils_1 = require("./utils");
8
7
  function registerSubscriptionManagementCommands(deps) {
@@ -24,7 +23,7 @@ function registerListCommand(deps) {
24
23
  .example('rsso.list')
25
24
  .action(async ({ session }, id) => {
26
25
  const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
27
- const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
26
+ const rssList = await getGuildSubscriptions(deps.store, platform, guildId);
28
27
  if (id === undefined) {
29
28
  if (rssList.length === 0)
30
29
  return '当前没有任何订阅';
@@ -70,14 +69,14 @@ function registerRemoveCommand(deps) {
70
69
  const authorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.authority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.authority} 或以上`);
71
70
  if (!authorityCheck.success)
72
71
  return authorityCheck.message;
73
- await deps.ctx.database.remove('rssOwl', { platform, guildId });
72
+ await deps.store.removeAllByGuild(platform, guildId);
74
73
  return '✅ 已删除全部订阅';
75
74
  }
76
- const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
75
+ const rssList = await getGuildSubscriptions(deps.store, platform, guildId);
77
76
  const rssItem = getSubscriptionByIndex(rssList, id);
78
77
  if (!rssItem)
79
78
  return getSubscriptionNotFoundMessage(id, rssList.length);
80
- await deps.ctx.database.remove('rssOwl', { id: rssItem.id });
79
+ await deps.store.remove(rssItem.id);
81
80
  return `✅ 已删除订阅: ${rssItem.title}`;
82
81
  });
83
82
  }
@@ -93,7 +92,7 @@ function registerPullCommand(deps) {
93
92
  .example('rsso.pull 1')
94
93
  .action(async ({ session }, id) => {
95
94
  const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
96
- const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
95
+ const rssList = await getGuildSubscriptions(deps.store, platform, guildId);
97
96
  const rssItem = getSubscriptionByIndex(rssList, id);
98
97
  if (!rssItem)
99
98
  return getSubscriptionNotFoundMessage(id, rssList.length);
@@ -121,7 +120,6 @@ function registerPullCommand(deps) {
121
120
  catch (error) {
122
121
  const normalizedError = (0, error_handler_1.normalizeError)(error);
123
122
  (0, logger_1.debug)(deps.config, normalizedError, 'pull error', 'error', logContext);
124
- (0, error_tracker_1.trackError)(normalizedError, logContext);
125
123
  return `拉取失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, '获取订阅数据')}`;
126
124
  }
127
125
  });
@@ -140,7 +138,7 @@ function registerFollowCommand(deps) {
140
138
  .example('rsso.follow 1')
141
139
  .action(async ({ session, options }, id) => {
142
140
  const { guildId, platform, authorId, authority } = (0, utils_1.extractSessionInfo)(session);
143
- const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
141
+ const rssList = await getGuildSubscriptions(deps.store, platform, guildId);
144
142
  const rssItem = getSubscriptionByIndex(rssList, id);
145
143
  if (!rssItem)
146
144
  return getSubscriptionNotFoundMessage(id, rssList.length);
@@ -152,18 +150,18 @@ function registerFollowCommand(deps) {
152
150
  if (followers.includes('all'))
153
151
  return '💡 已经设置全员提醒';
154
152
  followers.push('all');
155
- await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
153
+ await deps.store.update(rssItem.id, { followers });
156
154
  return '✅ 已设置全员提醒';
157
155
  }
158
156
  if (followers.includes(authorId))
159
157
  return '💡 已经关注过了';
160
158
  followers.push(authorId);
161
- await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
159
+ await deps.store.update(rssItem.id, { followers });
162
160
  return '✅ 关注成功';
163
161
  });
164
162
  }
165
- async function getGuildSubscriptions(ctx, platform, guildId) {
166
- return ctx.database.get('rssOwl', { platform, guildId });
163
+ async function getGuildSubscriptions(store, platform, guildId) {
164
+ return store.findByGuild(platform, guildId);
167
165
  }
168
166
  function getSubscriptionByIndex(rssList, id) {
169
167
  const listIndex = id - 1;
@@ -33,11 +33,6 @@ export declare function extractSessionInfo(session: Session): SessionInfo;
33
33
  * 构建命令日志上下文
34
34
  */
35
35
  export declare function buildCommandLogContext(session: Session, command?: string, operation?: string): Record<string, any>;
36
- /**
37
- * 命令错误处理包装器
38
- * 统一处理命令执行中的错误
39
- */
40
- export declare function withCommandErrorHandling(config: Config, operation: string, handler: () => Promise<string>, context?: Record<string, any>): Promise<string>;
41
36
  /**
42
37
  * 权限检查辅助函数
43
38
  */
@@ -6,14 +6,10 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.extractSessionInfo = extractSessionInfo;
8
8
  exports.buildCommandLogContext = buildCommandLogContext;
9
- exports.withCommandErrorHandling = withCommandErrorHandling;
10
9
  exports.checkAuthority = checkAuthority;
11
10
  exports.parseTarget = parseTarget;
12
11
  exports.parseTargets = parseTargets;
13
12
  exports.isValidUrl = isValidUrl;
14
- const error_handler_1 = require("../utils/error-handler");
15
- const error_tracker_1 = require("../utils/error-tracker");
16
- const logger_1 = require("../utils/logger");
17
13
  /**
18
14
  * 从会话中提取信息
19
15
  */
@@ -39,18 +35,6 @@ function buildCommandLogContext(session, command, operation) {
39
35
  context.operation = operation;
40
36
  return context;
41
37
  }
42
- /**
43
- * 命令错误处理包装器
44
- * 统一处理命令执行中的错误
45
- */
46
- function withCommandErrorHandling(config, operation, handler, context) {
47
- return handler().catch((error) => {
48
- const normalizedError = (0, error_handler_1.normalizeError)(error);
49
- (0, logger_1.debug)(config, normalizedError, `${operation} error`, 'error', context);
50
- (0, error_tracker_1.trackError)(normalizedError, context);
51
- return Promise.resolve(`${operation}失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, operation)}`);
52
- });
53
- }
54
38
  /**
55
39
  * 权限检查辅助函数
56
40
  */
@@ -1,4 +1,5 @@
1
1
  import { Context } from 'koishi';
2
+ import { SubscriptionStore } from '../core/subscription-store';
2
3
  import type { Config, TemplateType, rssArg } from '../types';
3
4
  type DebugType = 'disable' | 'error' | 'info' | 'details';
4
5
  type HtmlMonitorArg = rssArg & {
@@ -21,6 +22,7 @@ interface WatchCommandOptions {
21
22
  export interface WebMonitorCommandDeps {
22
23
  ctx: Context;
23
24
  config: Config;
25
+ store: SubscriptionStore;
24
26
  debug: (message: any, name?: string, type?: DebugType, context?: Record<string, any>) => void;
25
27
  mixinArg: (arg: Record<string, any>) => rssArg;
26
28
  getRssData: (url: string, arg: Record<string, any>) => Promise<any[]>;
@@ -55,7 +55,7 @@ HTML 网页监控功能,使用 CSS 选择器提取内容
55
55
  return '❌ 未找到符合选择器的元素';
56
56
  return buildItemsPreview(items, `找到 ${items.length} 个元素:`, true);
57
57
  }
58
- const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
58
+ const rssList = await deps.store.findByGuild(platform, guildId);
59
59
  if (rssList.find(item => item.url === normalizedUrl))
60
60
  return '❌ 该订阅已存在';
61
61
  const htmlItems = await deps.getRssData(normalizedUrl, arg);
@@ -77,7 +77,7 @@ HTML 网页监控功能,使用 CSS 选择器提取内容
77
77
  if (deps.config.basic.urlDeduplication && rssList.find(item => item.rssId === rssItem.rssId)) {
78
78
  return `❌ 订阅已存在: ${rssItem.rssId}`;
79
79
  }
80
- await deps.ctx.database.create('rssOwl', rssItem);
80
+ await deps.store.create(rssItem);
81
81
  if (deps.config.basic.firstLoad && arg.firstLoad !== false && htmlItems.length > 0) {
82
82
  await broadcastInitialItems(deps, `${platform}:${guildId}`, htmlItems, rssItem);
83
83
  }
package/lib/config.js CHANGED
@@ -54,10 +54,12 @@ exports.Config = koishi_1.Schema.object({
54
54
  maxRssItem: koishi_1.Schema.number().description('限制更新时的最大推送数量上限,超出上限时较早的更新会被忽略').default(10),
55
55
  firstLoad: koishi_1.Schema.boolean().description('首次订阅时是否发送最后的更新').default(true),
56
56
  urlDeduplication: koishi_1.Schema.boolean().description('同群组中不允许重复添加相同订阅').default(true),
57
- resendUpdataContent: koishi_1.Schema.union(['disable', 'latest', 'all']).description('当内容更新时再次发送').default('disable').experimental(),
57
+ resendUpdatedContent: koishi_1.Schema.union(['disable', 'latest', 'all']).description('当内容更新时再次发送').default('disable'),
58
+ resendUpdataContent: koishi_1.Schema.union(['disable', 'latest', 'all']).description('(已弃用,请使用 resendUpdatedContent)').hidden().deprecated(),
58
59
  imageMode: koishi_1.Schema.union(['base64', 'File', 'assets']).description('图片发送模式<br>\`base64\` Base64格式(兼容性好但容易超长)<br>\`File\` 本地文件(不支持沙盒环境)<br>\`assets\` Assets服务(推荐,需安装assets-xxx插件并配置)').default('base64'),
59
60
  videoMode: koishi_1.Schema.union(['filter', 'href', 'base64', 'File', 'assets']).description('视频发送模式(iframe标签内的视频无法处理)<br>\`filter\` 过滤视频,含有视频的推送将不会被发送<br>\`href\` 使用视频网络地址直接发送<br>\`base64\` 下载后以base64格式发送<br>\`File\` 下载后以文件发送<br>\`assets\` 上传到assets服务(需安装assets-xxx插件并配置)').default('href'),
60
- margeVideo: koishi_1.Schema.boolean().default(false).description('以合并消息发送视频'),
61
+ mergeVideo: koishi_1.Schema.boolean().default(false).description('以合并消息发送视频'),
62
+ margeVideo: koishi_1.Schema.boolean().description('(已弃用,请使用 mergeVideo)').hidden().deprecated(),
61
63
  usePoster: koishi_1.Schema.boolean().default(false).description('加载视频封面'),
62
64
  autoSplitImage: koishi_1.Schema.boolean().description('垂直拆分大尺寸图片,解决部分适配器发不出长图的问题').default(true),
63
65
  cacheDir: koishi_1.Schema.string().description('File模式时使用的缓存路径').default('data/cache/rssOwl'),
@@ -191,14 +193,6 @@ exports.Config = koishi_1.Schema.object({
191
193
  contextFields: koishi_1.Schema.array(koishi_1.Schema.string()).description('要包含的上下文字段(如 guildId, platform 等)').default([]),
192
194
  sanitizeLogs: koishi_1.Schema.boolean().description('自动脱敏日志中的敏感信息').default(true),
193
195
  }).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.48'),
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('错误追踪设置'),
202
196
  tdl: koishi_1.Schema.object({
203
197
  enabled: koishi_1.Schema.boolean().description('启用 Telegram 大视频 tdl 兜底下载<br>当 RSSHub 返回 "Video is too big" 占位时,调用外部 tdl 直接从 Telegram 拉取原始视频。<br><b>需宿主机自行安装</b>: `go install github.com/iyear/tdl@latest` 或下载 [Release](https://github.com/iyear/tdl/releases),并执行 `tdl login` 完成登录。<br>插件运行时探测 PATH,缺失则跳过、不阻塞主流程。').default(false),
204
198
  binPath: koishi_1.Schema.string().description('tdl 二进制路径,默认从 PATH 探测<br>Docker/容器场景下若 tdl 装在持久卷(如 /koishi/bin/tdl),在此指定可避免依赖容器 PATH').default(''),
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatArg = formatArg;
4
4
  exports.mixinArg = mixinArg;
5
5
  const legacy_config_1 = require("../utils/legacy-config");
6
+ const proxy_1 = require("../utils/proxy");
6
7
  const logger_1 = require("../utils/logger");
7
8
  const ARG_ENTRY_KEYS = [
8
9
  'forceLength',
@@ -102,45 +103,9 @@ function parseProxyAgentArg(value, auth) {
102
103
  return undefined;
103
104
  }
104
105
  }
105
- function mergeProxyAgent(argProxy, configProxy, config) {
106
- (0, logger_1.debug)(config, `合并代理配置 - argProxy: ${JSON.stringify(argProxy)}, configProxy.enabled: ${configProxy?.enabled}`, 'proxy merge debug', 'details');
107
- if (argProxy?.enabled === false) {
108
- (0, logger_1.debug)(config, '订阅明确禁用代理', 'proxy merge', 'details');
109
- return { enabled: false };
110
- }
111
- if (argProxy?.enabled === true && argProxy?.host) {
112
- (0, logger_1.debug)(config, '使用订阅的代理配置', 'proxy merge', 'details');
113
- return argProxy;
114
- }
115
- const shouldUseConfigProxy = !argProxy || Object.keys(argProxy || {}).length === 0 || argProxy?.enabled === undefined || argProxy?.enabled === null;
116
- if (shouldUseConfigProxy) {
117
- if (configProxy?.enabled) {
118
- const result = {
119
- enabled: true,
120
- protocol: configProxy.protocol,
121
- host: configProxy.host,
122
- port: configProxy.port,
123
- auth: configProxy.auth?.enabled ? configProxy.auth : undefined,
124
- };
125
- (0, logger_1.debug)(config, `使用全局代理: ${result.protocol}://${result.host}:${result.port}`, 'proxy merge', 'info');
126
- return result;
127
- }
128
- (0, logger_1.debug)(config, '全局代理未启用', 'proxy merge', 'details');
129
- }
130
- if (argProxy?.enabled === true && !argProxy?.host) {
131
- const result = {
132
- ...configProxy,
133
- ...argProxy,
134
- auth: configProxy?.auth?.enabled ? configProxy.auth : undefined,
135
- };
136
- (0, logger_1.debug)(config, '订阅代理配置不完整,补充全局配置', 'proxy merge', 'details');
137
- return result;
138
- }
139
- (0, logger_1.debug)(config, '代理未配置,使用默认(禁用)', 'proxy merge', 'details');
140
- return { enabled: false };
141
- }
106
+ // mergeProxyAgent 已提升到 utils/proxy.ts,统一代理 helper 归属
142
107
  function mergeProxyAgentWithLog(argProxy, configProxy, config) {
143
- const result = mergeProxyAgent(argProxy, configProxy, config);
108
+ const result = (0, proxy_1.mergeProxyAgent)(argProxy, configProxy, config);
144
109
  (0, logger_1.debug)(config, `[DEBUG_PROXY] mergeProxyAgent input: arg=${JSON.stringify(argProxy)} conf=${JSON.stringify(configProxy)} output=${JSON.stringify(result)}`, 'proxy merge', 'details');
145
110
  return result;
146
111
  }
@@ -13,7 +13,6 @@ const koishi_1 = require("koishi");
13
13
  const constants_1 = require("../constants");
14
14
  const common_1 = require("../utils/common");
15
15
  const error_handler_1 = require("../utils/error-handler");
16
- const error_tracker_1 = require("../utils/error-tracker");
17
16
  const legacy_config_1 = require("../utils/legacy-config");
18
17
  const logger_1 = require("../utils/logger");
19
18
  const parser_1 = require("./parser");
@@ -101,10 +100,6 @@ async function fetchRssItems(ctx, config, $http, rssItem, arg, feedDebug) {
101
100
  feedDebug(`Fetch failed for ${rssItem.title}: ${normalizedError.message}`, 'feeder', 'error', {
102
101
  stage: 'fetch',
103
102
  });
104
- (0, error_tracker_1.trackError)(normalizedError, {
105
- ...buildFeedLogContext(rssItem),
106
- stage: 'fetch',
107
- });
108
103
  return [];
109
104
  }
110
105
  }
@@ -40,7 +40,6 @@ exports.stopFeeder = stopFeeder;
40
40
  const koishi_1 = require("koishi");
41
41
  const common_1 = require("../utils/common");
42
42
  const error_handler_1 = require("../utils/error-handler");
43
- const error_tracker_1 = require("../utils/error-tracker");
44
43
  const legacy_config_1 = require("../utils/legacy-config");
45
44
  const logger_1 = require("../utils/logger");
46
45
  const feeder_arg_1 = require("./feeder-arg");
@@ -54,6 +53,12 @@ Object.defineProperty(exports, "findRssItem", { enumerable: true, get: function
54
53
  Object.defineProperty(exports, "getLastContent", { enumerable: true, get: function () { return feeder_runtime_2.getLastContent; } });
55
54
  let interval = null;
56
55
  let queueInterval = null;
56
+ let cleanupInterval = null;
57
+ // 重入栅栏:feeder() 单轮可能跑很久(订阅多 + 网络/tdl 慢),
58
+ // 若耗时超过 refreshInterval,下一次 interval tick 会并发启动第二个 feeder(),
59
+ // 导致同一消息被重复入队(addTask 的读-写去重非原子)→ 重复推送。
60
+ // 仿照 NotificationQueueManager.processQueue 的 processing 标志,单轮串行。
61
+ let feederRunning = false;
57
62
  function shouldSkipByInterval(rssItem, arg, originalArg) {
58
63
  if (!rssItem.arg.interval)
59
64
  return false;
@@ -171,24 +176,44 @@ async function feeder(deps, processor) {
171
176
  const normalizedError = (0, error_handler_1.normalizeError)(err);
172
177
  const feedContext = (0, feeder_runtime_1.buildFeedLogContext)(rssItem);
173
178
  (0, logger_1.debug)(config, `Feeder error for ${rssItem.url}: ${normalizedError.message}`, 'feeder', 'error', feedContext);
174
- (0, error_tracker_1.trackError)(normalizedError, feedContext);
175
179
  }
176
180
  }
177
181
  }
178
182
  function startFeeder(ctx, config, $http, processor, queueManager) {
183
+ // 幂等清理:若上一实例的 interval 尚未清理(未走 dispose 就再次 startFeeder),
184
+ // 先清掉旧句柄,避免 ghost 定时器泄漏。
185
+ if (interval) {
186
+ clearInterval(interval);
187
+ interval = null;
188
+ }
189
+ if (queueInterval) {
190
+ clearInterval(queueInterval);
191
+ queueInterval = null;
192
+ }
193
+ if (cleanupInterval) {
194
+ clearInterval(cleanupInterval);
195
+ cleanupInterval = null;
196
+ }
179
197
  const deps = { ctx, config, $http, queueManager };
180
198
  const lifecycleDebug = (0, logger_1.createDebugWithContext)(config, { lifecycle: 'feeder' });
181
199
  const queueRuntimeConfig = (0, notification_queue_retry_1.getQueueRuntimeConfig)(config);
200
+ // 受重入栅栏保护的 feeder 运行:避免上一轮未跑完时下一轮并发启动
201
+ const runFeederGuarded = () => {
202
+ if (feederRunning) {
203
+ (0, logger_1.debug)(config, 'feeder 上一轮仍在运行,跳过本次 tick', 'feeder', 'details', { skipped: true });
204
+ return Promise.resolve();
205
+ }
206
+ feederRunning = true;
207
+ return feeder(deps, processor).finally(() => {
208
+ feederRunning = false;
209
+ });
210
+ };
182
211
  // Initial run
183
- feeder(deps, processor).catch(err => {
212
+ runFeederGuarded().catch(err => {
184
213
  const normalizedError = (0, error_handler_1.normalizeError)(err);
185
214
  lifecycleDebug(`Initial feeder run failed: ${normalizedError.message}`, 'feeder', 'error', {
186
215
  operation: 'initial-feeder-run',
187
216
  });
188
- (0, error_tracker_1.trackError)(normalizedError, {
189
- lifecycle: 'feeder',
190
- operation: 'initial-feeder-run',
191
- });
192
217
  });
193
218
  // 启动生产者定时器(抓取 RSS)
194
219
  const refreshInterval = (config.basic?.refresh || 600) * 1000;
@@ -197,7 +222,7 @@ function startFeeder(ctx, config, $http, processor, queueManager) {
197
222
  const { delCache } = await Promise.resolve().then(() => __importStar(require('../utils/media')));
198
223
  await delCache(config);
199
224
  }
200
- await feeder(deps, processor);
225
+ await runFeederGuarded();
201
226
  }, refreshInterval);
202
227
  // 启动消费者定时器(处理发送队列)
203
228
  // 频率更高,确保消息快速发送
@@ -205,16 +230,26 @@ function startFeeder(ctx, config, $http, processor, queueManager) {
205
230
  queueInterval = setInterval(async () => {
206
231
  await queueManager.processQueue();
207
232
  }, queueProcessInterval);
233
+ // 启动清理定时器:周期性删除过期的 SUCCESS/FAILED 任务,
234
+ // 防止 rss_notification_queue 表无限膨胀拖慢查询(此前只手动命令清理)。
235
+ // 每小时一次,cleanupHours 控制具体保留时长。
236
+ cleanupInterval = setInterval(async () => {
237
+ try {
238
+ await queueManager.runAutomaticCleanup();
239
+ }
240
+ catch (err) {
241
+ const normalizedError = (0, error_handler_1.normalizeError)(err);
242
+ lifecycleDebug(`自动清理失败: ${normalizedError.message}`, 'queue', 'error', {
243
+ operation: 'automatic-cleanup',
244
+ });
245
+ }
246
+ }, 60 * 60 * 1000);
208
247
  // 立即处理一次队列(启动时)
209
248
  queueManager.processQueue().catch(err => {
210
249
  const normalizedError = (0, error_handler_1.normalizeError)(err);
211
250
  lifecycleDebug(`Initial queue processing failed: ${normalizedError.message}`, 'queue', 'error', {
212
251
  operation: 'initial-queue-processing',
213
252
  });
214
- (0, error_tracker_1.trackError)(normalizedError, {
215
- lifecycle: 'feeder',
216
- operation: 'initial-queue-processing',
217
- });
218
253
  });
219
254
  lifecycleDebug('Feeder started', 'feeder', 'info', {
220
255
  refreshInterval,
@@ -230,6 +265,12 @@ function stopFeeder(config) {
230
265
  clearInterval(queueInterval);
231
266
  queueInterval = null;
232
267
  }
268
+ if (cleanupInterval) {
269
+ clearInterval(cleanupInterval);
270
+ cleanupInterval = null;
271
+ }
272
+ // 复位栅栏:避免上一轮未跑完就 stopFeeder 后,下次 startFeeder 时栅栏卡死
273
+ feederRunning = false;
233
274
  if (config) {
234
275
  const lifecycleDebug = (0, logger_1.createDebugWithContext)(config, { lifecycle: 'feeder' });
235
276
  lifecycleDebug('Feeder stopped', 'feeder', 'info');
@@ -1,6 +1,7 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { Context } from 'koishi';
3
3
  import { Config, rssArg } from '../types';
4
+ import { normalizeText } from '../utils/common';
4
5
  export interface ItemProcessorRuntimeDeps {
5
6
  ctx: Context;
6
7
  config: Config;
@@ -13,8 +14,10 @@ interface RenderDescriptionOptions {
13
14
  }
14
15
  /**
15
16
  * 标准化 RSS 字段内容,统一数组 / 空值 / 非字符串输入。
17
+ *
18
+ * 实现已收敛到 `utils/common.ts`,此处 re-export 以维持现有 import 路径不变。
16
19
  */
17
- export declare function normalizeText(value: unknown): string;
20
+ export { normalizeText };
18
21
  /**
19
22
  * 解析并去重 HTML 中的图片资源,返回原图到最终地址的映射表。
20
23
  */
@@ -48,4 +51,3 @@ export declare function formatVideoList(videoList: Array<[string, string]>): str
48
51
  * 统一执行图片渲染,兼容 base64 / File / assets / HTML 回退。
49
52
  */
50
53
  export declare function renderImage(htmlContent: string, deps: ItemProcessorRuntimeDeps, arg: rssArg): Promise<string>;
51
- export {};
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.normalizeText = normalizeText;
36
+ exports.normalizeText = void 0;
37
37
  exports.buildResolvedImageMap = buildResolvedImageMap;
38
38
  exports.renderImageListFromHtml = renderImageListFromHtml;
39
39
  exports.renderLoadedHtml = renderLoadedHtml;
@@ -44,6 +44,8 @@ exports.renderImage = renderImage;
44
44
  const cheerio = __importStar(require("cheerio"));
45
45
  const koishi_1 = require("koishi");
46
46
  const marked_1 = require("marked");
47
+ const common_1 = require("../utils/common");
48
+ Object.defineProperty(exports, "normalizeText", { enumerable: true, get: function () { return common_1.normalizeText; } });
47
49
  const logger_1 = require("../utils/logger");
48
50
  const media_1 = require("../utils/media");
49
51
  const renderer_1 = require("./renderer");
@@ -62,7 +64,7 @@ function collectUniqueImageSources(html) {
62
64
  return [...new Set(imageSources)];
63
65
  }
64
66
  async function prependAiSummarySection(config, htmlContent, item, options) {
65
- const aiSummary = normalizeText(item?.aiSummary).trim();
67
+ const aiSummary = (0, common_1.normalizeText)(item?.aiSummary).trim();
66
68
  if (!aiSummary || !isImageRenderEnabled(config))
67
69
  return htmlContent;
68
70
  const aiSummaryHtml = await (0, marked_1.marked)(aiSummary);
@@ -99,16 +101,6 @@ async function prepareHtmlForRender(deps, html, arg, useXml = false) {
99
101
  html('img').attr('style', 'object-fit:scale-down;max-width:100%;height:auto;');
100
102
  return useXml ? html.xml() : html.html();
101
103
  }
102
- /**
103
- * 标准化 RSS 字段内容,统一数组 / 空值 / 非字符串输入。
104
- */
105
- function normalizeText(value) {
106
- if (Array.isArray(value))
107
- return value.join('');
108
- if (value === undefined || value === null)
109
- return '';
110
- return String(value);
111
- }
112
104
  /**
113
105
  * 解析并去重 HTML 中的图片资源,返回原图到最终地址的映射表。
114
106
  */
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.NotificationQueueSender = void 0;
37
37
  exports.downgradeQueueMessage = downgradeQueueMessage;
38
38
  const error_handler_1 = require("../utils/error-handler");
39
- const error_tracker_1 = require("../utils/error-tracker");
40
39
  const notification_queue_retry_1 = require("./notification-queue-retry");
41
40
  function downgradeQueueMessage(content) {
42
41
  if (content.isDowngraded) {
@@ -108,10 +107,6 @@ class NotificationQueueSender {
108
107
  catch (err) {
109
108
  const normalizedError = (0, error_handler_1.normalizeError)(err);
110
109
  taskDebug(`缓存消息失败: ${normalizedError.message}`, 'cache', 'info');
111
- (0, error_tracker_1.trackError)(normalizedError, {
112
- ...this.deps.buildTaskLogContext(task),
113
- operation: 'cacheMessage',
114
- });
115
110
  }
116
111
  }
117
112
  }