@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
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSubscriptionManagementCommands = registerSubscriptionManagementCommands;
4
+ const error_handler_1 = require("../utils/error-handler");
5
+ const error_tracker_1 = require("../utils/error-tracker");
6
+ const logger_1 = require("../utils/logger");
7
+ const utils_1 = require("./utils");
8
+ function registerSubscriptionManagementCommands(deps) {
9
+ registerListCommand(deps);
10
+ registerRemoveCommand(deps);
11
+ registerPullCommand(deps);
12
+ registerFollowCommand(deps);
13
+ }
14
+ function registerListCommand(deps) {
15
+ deps.ctx.guild()
16
+ .command('rssowl.list [id:number]', '查看订阅列表')
17
+ .alias('rsso.list')
18
+ .usage(`查看订阅列表
19
+
20
+ 用法:
21
+ rsso.list - 查看所有订阅
22
+ rsso.list 1 - 查看订阅 #1 的详情(使用列表序号)
23
+ `)
24
+ .example('rsso.list')
25
+ .action(async ({ session }, id) => {
26
+ const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
27
+ const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
28
+ if (id === undefined) {
29
+ if (rssList.length === 0)
30
+ return '当前没有任何订阅';
31
+ return rssList.map((item, index) => {
32
+ const isCrossGroup = item.platform !== platform || item.guildId !== guildId;
33
+ return `${index + 1}. ${item.title}${isCrossGroup ? ' [跨群]' : ''} [ID:${item.id}]`;
34
+ }).join('\n');
35
+ }
36
+ const rssItem = getSubscriptionByIndex(rssList, id);
37
+ if (!rssItem)
38
+ return getSubscriptionNotFoundMessage(id, rssList.length);
39
+ const followers = rssItem.followers?.length > 0 ? rssItem.followers.join(', ') : '无';
40
+ const pushTarget = `${rssItem.platform}:${rssItem.guildId}`;
41
+ const isCrossGroup = rssItem.platform !== platform || rssItem.guildId !== guildId;
42
+ const targetInfo = isCrossGroup
43
+ ? `📤 推送目标: ${pushTarget} (跨群订阅)`
44
+ : `📤 推送目标: ${pushTarget} (本群)`;
45
+ return `📰 订阅详情 [序号:${id} | ID:${rssItem.id}]
46
+ 标题: ${rssItem.title}
47
+ 链接: ${rssItem.url}
48
+ 类型: ${rssItem.arg?.type || 'RSS'}
49
+ 模板: ${rssItem.arg?.template || deps.config.basic.defaultTemplate}
50
+ ${targetInfo}
51
+ 更新时间: ${rssItem.lastPubDate ? deps.parsePubDate(rssItem.lastPubDate).toLocaleString('zh-CN', { hour12: false }) : '未知'}
52
+ 关注者: ${followers}`;
53
+ });
54
+ }
55
+ function registerRemoveCommand(deps) {
56
+ deps.ctx.guild()
57
+ .command('rssowl.remove <id:number>', '删除订阅')
58
+ .alias('rsso.remove')
59
+ .usage(`删除订阅
60
+
61
+ 用法:
62
+ rsso.remove 1 - 删除订阅 #1(使用列表序号)
63
+ rsso.remove --all - 删除全部订阅(需要权限)
64
+ `)
65
+ .option('all', '--all 删除全部订阅')
66
+ .example('rsso.remove 1')
67
+ .action(async ({ session, options }, id) => {
68
+ const { guildId, platform, authority } = (0, utils_1.extractSessionInfo)(session);
69
+ if (options.all) {
70
+ const authorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.authority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.authority} 或以上`);
71
+ if (!authorityCheck.success)
72
+ return authorityCheck.message;
73
+ await deps.ctx.database.remove('rssOwl', { platform, guildId });
74
+ return '✅ 已删除全部订阅';
75
+ }
76
+ const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
77
+ const rssItem = getSubscriptionByIndex(rssList, id);
78
+ if (!rssItem)
79
+ return getSubscriptionNotFoundMessage(id, rssList.length);
80
+ await deps.ctx.database.remove('rssOwl', { id: rssItem.id });
81
+ return `✅ 已删除订阅: ${rssItem.title}`;
82
+ });
83
+ }
84
+ function registerPullCommand(deps) {
85
+ deps.ctx.guild()
86
+ .command('rssowl.pull <id:number>', '拉取订阅最新内容')
87
+ .alias('rsso.pull')
88
+ .usage(`拉取订阅最新内容
89
+
90
+ 用法:
91
+ rsso.pull 1 - 拉取订阅 #1 的最新更新(使用列表序号)
92
+ `)
93
+ .example('rsso.pull 1')
94
+ .action(async ({ session }, id) => {
95
+ const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
96
+ const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
97
+ const rssItem = getSubscriptionByIndex(rssList, id);
98
+ if (!rssItem)
99
+ return getSubscriptionNotFoundMessage(id, rssList.length);
100
+ const logContext = {
101
+ ...(0, utils_1.buildCommandLogContext)(session, 'rsso.pull', 'pull'),
102
+ subscriptionIndex: id,
103
+ subscribeId: String(rssItem.id),
104
+ rssId: rssItem.rssId || rssItem.title,
105
+ url: rssItem.url,
106
+ };
107
+ try {
108
+ const arg = deps.mixinArg(rssItem.arg || {});
109
+ const rssItemList = (await Promise.all(String(rssItem.url).split('|')
110
+ .map(url => deps.parseQuickUrl(url))
111
+ .map(async (url) => await deps.getRssData(url, arg)))).flat(1);
112
+ let itemArray = rssItemList.sort((a, b) => deps.parsePubDate(b.pubDate).getTime() - deps.parsePubDate(a.pubDate).getTime());
113
+ if (arg.reverse)
114
+ itemArray = itemArray.reverse();
115
+ const maxItem = arg.forceLength || 1;
116
+ const messageList = await Promise.all(itemArray
117
+ .filter((_, index) => index < maxItem)
118
+ .map(async (item) => await deps.parseRssItem(item, { ...rssItem, ...arg }, rssItem.author)));
119
+ return messageList.join('');
120
+ }
121
+ catch (error) {
122
+ const normalizedError = (0, error_handler_1.normalizeError)(error);
123
+ (0, logger_1.debug)(deps.config, normalizedError, 'pull error', 'error', logContext);
124
+ (0, error_tracker_1.trackError)(normalizedError, logContext);
125
+ return `拉取失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, '获取订阅数据')}`;
126
+ }
127
+ });
128
+ }
129
+ function registerFollowCommand(deps) {
130
+ deps.ctx.guild()
131
+ .command('rssowl.follow <id:number>', '关注订阅')
132
+ .alias('rsso.follow')
133
+ .usage(`关注订阅,在该订阅更新时提醒你
134
+
135
+ 用法:
136
+ rsso.follow 1 - 关注订阅 #1(仅提醒你)
137
+ rsso.follow 1 --all - 关注订阅 #1(提醒所有人,需要高级权限)
138
+ `)
139
+ .option('all', '--all 提醒所有人')
140
+ .example('rsso.follow 1')
141
+ .action(async ({ session, options }, id) => {
142
+ const { guildId, platform, authorId, authority } = (0, utils_1.extractSessionInfo)(session);
143
+ const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
144
+ const rssItem = getSubscriptionByIndex(rssList, id);
145
+ if (!rssItem)
146
+ return getSubscriptionNotFoundMessage(id, rssList.length);
147
+ const followers = Array.isArray(rssItem.followers) ? [...rssItem.followers] : [];
148
+ if (options.all) {
149
+ const authorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.advancedAuthority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.advancedAuthority} 或以上`);
150
+ if (!authorityCheck.success)
151
+ return authorityCheck.message;
152
+ if (followers.includes('all'))
153
+ return '已经设置全员提醒';
154
+ followers.push('all');
155
+ await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
156
+ return '✅ 已设置全员提醒';
157
+ }
158
+ if (followers.includes(authorId))
159
+ return '已经关注过了';
160
+ followers.push(authorId);
161
+ await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
162
+ return '✅ 关注成功';
163
+ });
164
+ }
165
+ async function getGuildSubscriptions(ctx, platform, guildId) {
166
+ return ctx.database.get('rssOwl', { platform, guildId });
167
+ }
168
+ function getSubscriptionByIndex(rssList, id) {
169
+ const listIndex = id - 1;
170
+ if (listIndex < 0 || listIndex >= rssList.length)
171
+ return null;
172
+ return rssList[listIndex];
173
+ }
174
+ function getSubscriptionNotFoundMessage(id, total) {
175
+ return `❌ 序号 ${id} 不存在\n当前共有 ${total} 个订阅\n\n使用 rsso.list 查看完整列表`;
176
+ }
@@ -21,15 +21,23 @@ export interface SessionInfo {
21
21
  authorId: string;
22
22
  authority: number;
23
23
  }
24
+ export interface ParseTargetsResult {
25
+ targets: string[];
26
+ invalidTarget?: string;
27
+ }
24
28
  /**
25
29
  * 从会话中提取信息
26
30
  */
27
31
  export declare function extractSessionInfo(session: Session): SessionInfo;
32
+ /**
33
+ * 构建命令日志上下文
34
+ */
35
+ export declare function buildCommandLogContext(session: Session, command?: string, operation?: string): Record<string, any>;
28
36
  /**
29
37
  * 命令错误处理包装器
30
38
  * 统一处理命令执行中的错误
31
39
  */
32
- export declare function withCommandErrorHandling(config: Config, operation: string, handler: () => Promise<string>): Promise<string>;
40
+ export declare function withCommandErrorHandling(config: Config, operation: string, handler: () => Promise<string>, context?: Record<string, any>): Promise<string>;
33
41
  /**
34
42
  * 权限检查辅助函数
35
43
  */
@@ -44,6 +52,10 @@ export declare function parseTarget(target: string): {
44
52
  platform: string;
45
53
  guildId: string;
46
54
  } | null;
55
+ /**
56
+ * 解析多个推送目标
57
+ */
58
+ export declare function parseTargets(targetInput?: string): ParseTargetsResult;
47
59
  /**
48
60
  * 验证 URL 格式
49
61
  */
@@ -5,11 +5,14 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.extractSessionInfo = extractSessionInfo;
8
+ exports.buildCommandLogContext = buildCommandLogContext;
8
9
  exports.withCommandErrorHandling = withCommandErrorHandling;
9
10
  exports.checkAuthority = checkAuthority;
10
11
  exports.parseTarget = parseTarget;
12
+ exports.parseTargets = parseTargets;
11
13
  exports.isValidUrl = isValidUrl;
12
14
  const error_handler_1 = require("../utils/error-handler");
15
+ const error_tracker_1 = require("../utils/error-tracker");
13
16
  const logger_1 = require("../utils/logger");
14
17
  /**
15
18
  * 从会话中提取信息
@@ -21,13 +24,30 @@ function extractSessionInfo(session) {
21
24
  const { authority } = session.user;
22
25
  return { guildId, platform, authorId, authority };
23
26
  }
27
+ /**
28
+ * 构建命令日志上下文
29
+ */
30
+ function buildCommandLogContext(session, command, operation) {
31
+ const sessionInfo = extractSessionInfo(session);
32
+ const context = {
33
+ ...sessionInfo,
34
+ userId: sessionInfo.authorId,
35
+ };
36
+ if (command)
37
+ context.command = command;
38
+ if (operation)
39
+ context.operation = operation;
40
+ return context;
41
+ }
24
42
  /**
25
43
  * 命令错误处理包装器
26
44
  * 统一处理命令执行中的错误
27
45
  */
28
- function withCommandErrorHandling(config, operation, handler) {
46
+ function withCommandErrorHandling(config, operation, handler, context) {
29
47
  return handler().catch((error) => {
30
- (0, logger_1.debug)(config, error, `${operation} error`, '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);
31
51
  return Promise.resolve(`${operation}失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, operation)}`);
32
52
  });
33
53
  }
@@ -56,6 +76,27 @@ function parseTarget(target) {
56
76
  guildId: parts[1]
57
77
  };
58
78
  }
79
+ /**
80
+ * 解析多个推送目标
81
+ */
82
+ function parseTargets(targetInput) {
83
+ if (!targetInput) {
84
+ return { targets: [] };
85
+ }
86
+ const targets = targetInput
87
+ .split(/[;,,;]/)
88
+ .map(target => target.trim())
89
+ .filter(Boolean);
90
+ for (const target of targets) {
91
+ if (!parseTarget(target)) {
92
+ return {
93
+ targets: [],
94
+ invalidTarget: target,
95
+ };
96
+ }
97
+ }
98
+ return { targets };
99
+ }
59
100
  /**
60
101
  * 验证 URL 格式
61
102
  */
package/lib/config.js CHANGED
@@ -164,6 +164,16 @@ exports.Config = koishi_1.Schema.object({
164
164
  enabled: koishi_1.Schema.boolean().description('启用消息缓存').default(true),
165
165
  maxSize: koishi_1.Schema.number().description('最大缓存消息条数').default(100),
166
166
  }).description('消息缓存设置'),
167
+ security: koishi_1.Schema.object({
168
+ enabled: koishi_1.Schema.boolean().description('启用安全检查(建议开启)').default(false),
169
+ allowInternalAccess: koishi_1.Schema.boolean().description('允许访问内网 IP 地址(如本地部署的 RSSHub)').default(false),
170
+ whitelist: koishi_1.Schema.array(koishi_1.Schema.string()).description('URL 白名单域名').default([]),
171
+ blacklist: koishi_1.Schema.array(koishi_1.Schema.string()).description('URL 黑名单域名').default([]),
172
+ allowHttp: koishi_1.Schema.boolean().description('允许 HTTP 协议').default(true),
173
+ allowHttps: koishi_1.Schema.boolean().description('允许 HTTPS 协议').default(true),
174
+ sanitizeHtml: koishi_1.Schema.boolean().description('启用 RSS 原始 HTML 内容清理').default(true),
175
+ maxCacheSize: koishi_1.Schema.number().description('AI 摘要缓存最大条数').default(1000),
176
+ }).description('安全设置'),
167
177
  // customUrlEnable:Schema.boolean().description('开发中:允许使用自定义规则对网页进行提取,用于对非RSS链接抓取').default(false).experimental(),
168
178
  debug: koishi_1.Schema.union(["disable", "error", "info", "details"]).default("disable").description('调试级别'),
169
179
  logging: koishi_1.Schema.object({
@@ -173,5 +183,14 @@ exports.Config = koishi_1.Schema.object({
173
183
  includeModule: koishi_1.Schema.boolean().description('包含模块名').default(true),
174
184
  includeContext: koishi_1.Schema.boolean().description('包含额外上下文信息').default(false),
175
185
  contextFields: koishi_1.Schema.array(koishi_1.Schema.string()).description('要包含的上下文字段(如 guildId, platform 等)').default([]),
186
+ sanitizeLogs: koishi_1.Schema.boolean().description('自动脱敏日志中的敏感信息').default(true),
176
187
  }).description('日志设置'),
188
+ errorTracking: koishi_1.Schema.object({
189
+ enabled: koishi_1.Schema.boolean().description('启用错误追踪').default(false),
190
+ dsn: koishi_1.Schema.string().role('secret').description('Sentry DSN').default(''),
191
+ environment: koishi_1.Schema.string().description('错误追踪环境').default('production'),
192
+ release: koishi_1.Schema.string().description('错误追踪版本号').default('5.0.0-beta'),
193
+ tracesSampleRate: koishi_1.Schema.number().min(0).max(1).description('性能追踪采样率').default(0.1),
194
+ profilesSampleRate: koishi_1.Schema.number().min(0).max(1).description('性能分析采样率').default(0.1),
195
+ }).description('错误追踪设置'),
177
196
  });
package/lib/core/ai.d.ts CHANGED
@@ -5,7 +5,21 @@ import { Config } from '../types';
5
5
  export declare class AiSummaryCache {
6
6
  private cache;
7
7
  private ttl;
8
- constructor(ttl?: number);
8
+ private maxSize;
9
+ private accessOrder;
10
+ constructor(ttl?: number, maxSize?: number);
11
+ /**
12
+ * 触发 LRU 淘汰,移除最久未访问的条目
13
+ */
14
+ private evictIfNeeded;
15
+ /**
16
+ * 更新访问顺序(LRU)
17
+ */
18
+ private updateAccessOrder;
19
+ /**
20
+ * 从访问顺序中移除指定键
21
+ */
22
+ private removeAccessOrder;
9
23
  /**
10
24
  * 生成缓存键(基于内容的哈希)
11
25
  */
@@ -37,7 +51,7 @@ export declare class AiSummaryCache {
37
51
  /**
38
52
  * 初始化 AI 摘要缓存
39
53
  */
40
- export declare function initAiCache(ttl?: number): void;
54
+ export declare function initAiCache(ttl?: number, maxSize?: number): void;
41
55
  /**
42
56
  * 生成单条 AI 摘要(带缓存和降级)
43
57
  */
package/lib/core/ai.js CHANGED
@@ -58,8 +58,53 @@ const config_1 = require("../config");
58
58
  class AiSummaryCache {
59
59
  cache = new Map();
60
60
  ttl; // 缓存过期时间(毫秒)
61
- constructor(ttl = 24 * 60 * 60 * 1000) {
61
+ maxSize; // 最大缓存条数
62
+ accessOrder = [];
63
+ constructor(ttl = 24 * 60 * 60 * 1000, maxSize = 1000) {
62
64
  this.ttl = ttl;
65
+ this.maxSize = maxSize;
66
+ }
67
+ /**
68
+ * 触发 LRU 淘汰,移除最久未访问的条目
69
+ */
70
+ evictIfNeeded() {
71
+ // 如果缓存未达到最大限制,不需要淘汰
72
+ // 注意:这里用 > 而不是 >=,确保添加新项后不会超过限制
73
+ while (this.cache.size >= this.maxSize && this.accessOrder.length > 0) {
74
+ const oldestKey = this.accessOrder.shift();
75
+ if (oldestKey && this.cache.has(oldestKey)) {
76
+ this.cache.delete(oldestKey);
77
+ }
78
+ // 防止死循环,如果 accessOrder 为空但 cache 仍满,随机删除一个
79
+ if (this.accessOrder.length === 0 && this.cache.size >= this.maxSize) {
80
+ const randomKey = this.cache.keys().next().value;
81
+ if (randomKey) {
82
+ this.cache.delete(randomKey);
83
+ }
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * 更新访问顺序(LRU)
90
+ */
91
+ updateAccessOrder(key) {
92
+ // 从当前位置移除(如果存在)
93
+ const index = this.accessOrder.indexOf(key);
94
+ if (index > -1) {
95
+ this.accessOrder.splice(index, 1);
96
+ }
97
+ // 添加到末尾(最近访问)
98
+ this.accessOrder.push(key);
99
+ }
100
+ /**
101
+ * 从访问顺序中移除指定键
102
+ */
103
+ removeAccessOrder(key) {
104
+ const index = this.accessOrder.indexOf(key);
105
+ if (index > -1) {
106
+ this.accessOrder.splice(index, 1);
107
+ }
63
108
  }
64
109
  /**
65
110
  * 生成缓存键(基于内容的哈希)
@@ -82,8 +127,12 @@ class AiSummaryCache {
82
127
  // 检查是否过期
83
128
  if (Date.now() - entry.timestamp > this.ttl) {
84
129
  this.cache.delete(key);
130
+ this.removeAccessOrder(key);
85
131
  return null;
86
132
  }
133
+ // 更新访问顺序(LRU)
134
+ this.updateAccessOrder(key);
135
+ entry.lastAccess = Date.now();
87
136
  return entry.summary;
88
137
  }
89
138
  /**
@@ -91,10 +140,24 @@ class AiSummaryCache {
91
140
  */
92
141
  set(title, content, summary) {
93
142
  const key = this.generateKey(title, content);
143
+ // 如果已存在,更新访问顺序并更新时间戳
144
+ if (this.cache.has(key)) {
145
+ this.updateAccessOrder(key);
146
+ const entry = this.cache.get(key);
147
+ entry.summary = summary;
148
+ entry.timestamp = Date.now();
149
+ entry.lastAccess = Date.now();
150
+ return;
151
+ }
152
+ // 如果是新条目,先检查是否需要淘汰
153
+ this.evictIfNeeded();
94
154
  this.cache.set(key, {
95
155
  summary,
96
- timestamp: Date.now()
156
+ timestamp: Date.now(),
157
+ lastAccess: Date.now()
97
158
  });
159
+ // 添加到访问顺序
160
+ this.accessOrder.push(key);
98
161
  }
99
162
  /**
100
163
  * 清除过期缓存
@@ -104,6 +167,7 @@ class AiSummaryCache {
104
167
  for (const [key, entry] of this.cache.entries()) {
105
168
  if (now - entry.timestamp > this.ttl) {
106
169
  this.cache.delete(key);
170
+ this.removeAccessOrder(key);
107
171
  }
108
172
  }
109
173
  }
@@ -112,6 +176,7 @@ class AiSummaryCache {
112
176
  */
113
177
  clear() {
114
178
  this.cache.clear();
179
+ this.accessOrder = [];
115
180
  }
116
181
  /**
117
182
  * 获取缓存统计
@@ -129,10 +194,12 @@ let globalCache = null;
129
194
  /**
130
195
  * 初始化 AI 摘要缓存
131
196
  */
132
- function initAiCache(ttl) {
197
+ function initAiCache(ttl, maxSize) {
133
198
  if (!globalCache) {
134
- globalCache = new AiSummaryCache(ttl);
135
- (0, logger_1.debug)({ debug: 'info' }, 'AI 摘要缓存已初始化', 'AI-Cache', 'info');
199
+ // 使用配置中的 maxSize 或默认值 1000
200
+ const defaultMaxSize = 1000;
201
+ globalCache = new AiSummaryCache(ttl, maxSize || defaultMaxSize);
202
+ (0, logger_1.debug)({ debug: 'info' }, `AI 摘要缓存已初始化 (TTL: ${ttl || 24 * 60 * 60 * 1000}ms, MaxSize: ${maxSize || defaultMaxSize})`, 'AI-Cache', 'info');
136
203
  }
137
204
  }
138
205
  /**
@@ -235,7 +302,7 @@ async function getAiSummary(config, title, contentHtml) {
235
302
  return '';
236
303
  // 初始化缓存(如果还没初始化)
237
304
  if (!globalCache) {
238
- initAiCache();
305
+ initAiCache(undefined, config.security?.maxCacheSize);
239
306
  }
240
307
  // 清洗内容
241
308
  const plainText = cleanHtmlContent(contentHtml, config.ai.maxInputLength);
@@ -17,4 +17,4 @@ export declare function mixinArg(arg: any, config: Config): rssArg;
17
17
  */
18
18
  export declare function feeder(deps: FeederDependencies, processor: RssItemProcessor): Promise<void>;
19
19
  export declare function startFeeder(ctx: Context, config: Config, $http: any, processor: RssItemProcessor, queueManager: NotificationQueueManager): void;
20
- export declare function stopFeeder(): void;
20
+ export declare function stopFeeder(config?: Config): void;