@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.
- package/lib/commands/error-handler.js +2 -5
- package/lib/commands/index.d.ts +17 -1
- package/lib/commands/index.js +388 -2
- 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/config.js +19 -0
- package/lib/core/ai.d.ts +16 -2
- package/lib/core/ai.js +73 -6
- package/lib/core/feeder.d.ts +1 -1
- package/lib/core/feeder.js +238 -125
- package/lib/core/item-processor.d.ts +5 -0
- package/lib/core/item-processor.js +66 -136
- package/lib/core/notification-queue.d.ts +2 -0
- package/lib/core/notification-queue.js +80 -33
- package/lib/core/parser.js +12 -0
- package/lib/core/renderer.d.ts +15 -0
- package/lib/core/renderer.js +91 -23
- package/lib/index.js +28 -784
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +24 -0
- 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/logger.d.ts +4 -2
- package/lib/utils/logger.js +144 -6
- package/lib/utils/media.js +3 -6
- 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.js +3 -20
- 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
|
+
}
|
package/lib/commands/utils.d.ts
CHANGED
|
@@ -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
|
*/
|
package/lib/commands/utils.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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);
|
package/lib/core/feeder.d.ts
CHANGED
|
@@ -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;
|