@anyul/koishi-plugin-rss 5.2.2 → 5.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -37
- package/lib/commands/error-handler.js +13 -4
- package/lib/commands/index.d.ts +20 -1
- package/lib/commands/index.js +394 -2
- package/lib/commands/runtime.d.ts +17 -0
- package/lib/commands/runtime.js +27 -0
- package/lib/commands/subscription-create.d.ts +23 -0
- package/lib/commands/subscription-create.js +145 -0
- package/lib/commands/subscription-edit.d.ts +7 -0
- package/lib/commands/subscription-edit.js +177 -0
- package/lib/commands/subscription-management.d.ts +12 -0
- package/lib/commands/subscription-management.js +176 -0
- package/lib/commands/utils.d.ts +13 -1
- package/lib/commands/utils.js +43 -2
- package/lib/commands/web-monitor.d.ts +15 -0
- package/lib/commands/web-monitor.js +222 -0
- package/lib/config.js +25 -0
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +46 -83
- package/lib/core/ai-cache.d.ts +27 -0
- package/lib/core/ai-cache.js +169 -0
- package/lib/core/ai-client.d.ts +12 -0
- package/lib/core/ai-client.js +65 -0
- package/lib/core/ai-selector.d.ts +2 -0
- package/lib/core/ai-selector.js +80 -0
- package/lib/core/ai-summary.d.ts +10 -0
- package/lib/core/ai-summary.js +73 -0
- package/lib/core/ai-utils.d.ts +10 -0
- package/lib/core/ai-utils.js +104 -0
- package/lib/core/ai.d.ts +3 -77
- package/lib/core/ai.js +13 -455
- package/lib/core/feeder-arg.d.ts +17 -0
- package/lib/core/feeder-arg.js +234 -0
- package/lib/core/feeder-runtime.d.ts +96 -0
- package/lib/core/feeder-runtime.js +233 -0
- package/lib/core/feeder.d.ts +4 -6
- package/lib/core/feeder.js +120 -304
- package/lib/core/item-processor-runtime.d.ts +46 -0
- package/lib/core/item-processor-runtime.js +215 -0
- package/lib/core/item-processor-template.d.ts +16 -0
- package/lib/core/item-processor-template.js +158 -0
- package/lib/core/item-processor.d.ts +1 -10
- package/lib/core/item-processor.js +48 -393
- package/lib/core/notification-queue-retry.d.ts +25 -0
- package/lib/core/notification-queue-retry.js +78 -0
- package/lib/core/notification-queue-sender.d.ts +20 -0
- package/lib/core/notification-queue-sender.js +118 -0
- package/lib/core/notification-queue-store.d.ts +19 -0
- package/lib/core/notification-queue-store.js +137 -0
- package/lib/core/notification-queue-types.d.ts +49 -0
- package/lib/core/notification-queue-types.js +2 -0
- package/lib/core/notification-queue.d.ts +13 -72
- package/lib/core/notification-queue.js +132 -262
- package/lib/core/parser.js +12 -0
- package/lib/core/renderer.d.ts +15 -0
- package/lib/core/renderer.js +91 -23
- package/lib/core/search-format.d.ts +3 -0
- package/lib/core/search-format.js +36 -0
- package/lib/core/search-providers.d.ts +13 -0
- package/lib/core/search-providers.js +175 -0
- package/lib/core/search-rotation.d.ts +4 -0
- package/lib/core/search-rotation.js +55 -0
- package/lib/core/search-service.d.ts +3 -0
- package/lib/core/search-service.js +100 -0
- package/lib/core/search-types.d.ts +39 -0
- package/lib/core/search-types.js +2 -0
- package/lib/core/search.d.ts +4 -101
- package/lib/core/search.js +10 -508
- package/lib/index.js +50 -1160
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +51 -6
- package/lib/utils/common.js +52 -3
- package/lib/utils/error-handler.d.ts +8 -0
- package/lib/utils/error-handler.js +27 -0
- package/lib/utils/error-tracker.js +24 -8
- package/lib/utils/fetcher.js +68 -9
- package/lib/utils/legacy-config.d.ts +12 -0
- package/lib/utils/legacy-config.js +56 -0
- package/lib/utils/logger.d.ts +4 -2
- package/lib/utils/logger.js +193 -34
- package/lib/utils/media.js +3 -6
- package/lib/utils/proxy.d.ts +3 -0
- package/lib/utils/proxy.js +14 -0
- package/lib/utils/sanitizer.d.ts +58 -0
- package/lib/utils/sanitizer.js +227 -0
- package/lib/utils/security.d.ts +75 -0
- package/lib/utils/security.js +312 -0
- package/lib/utils/structured-logger.d.ts +7 -3
- package/lib/utils/structured-logger.js +29 -39
- package/package.json +2 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecurityError = void 0;
|
|
4
|
+
exports.isInternalUrl = isInternalUrl;
|
|
5
|
+
exports.isAllowedUrl = isAllowedUrl;
|
|
6
|
+
exports.validateUrl = validateUrl;
|
|
7
|
+
exports.validateUrlOrThrow = validateUrlOrThrow;
|
|
8
|
+
exports.getSecurityOptions = getSecurityOptions;
|
|
9
|
+
exports.createSafeHttpFunction = createSafeHttpFunction;
|
|
10
|
+
/**
|
|
11
|
+
* 安全验证错误类
|
|
12
|
+
*/
|
|
13
|
+
class SecurityError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'SecurityError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.SecurityError = SecurityError;
|
|
20
|
+
// 内网 IP 范围定义
|
|
21
|
+
const INTERNAL_IP_RANGES = [
|
|
22
|
+
// IPv4 回环地址
|
|
23
|
+
{ start: '127.0.0.0', end: '127.255.255.255' },
|
|
24
|
+
// IPv4 私有地址 - 10.0.0.0/8
|
|
25
|
+
{ start: '10.0.0.0', end: '10.255.255.255' },
|
|
26
|
+
// IPv4 私有地址 - 172.16.0.0/12
|
|
27
|
+
{ start: '172.16.0.0', end: '172.31.255.255' },
|
|
28
|
+
// IPv4 私有地址 - 192.168.0.0/16
|
|
29
|
+
{ start: '192.168.0.0', end: '192.168.255.255' },
|
|
30
|
+
// IPv4 链路本地地址 - 169.254.0.0/16 (包含 169.254.169.254 云元数据)
|
|
31
|
+
{ start: '169.254.0.0', end: '169.254.255.255' },
|
|
32
|
+
// IPv4 广播地址
|
|
33
|
+
{ start: '255.255.255.255', end: '255.255.255.255' },
|
|
34
|
+
// IPv4 0.0.0.0
|
|
35
|
+
{ start: '0.0.0.0', end: '0.0.0.0' },
|
|
36
|
+
// IPv6 回环地址
|
|
37
|
+
{ start: '::1', end: '::1' },
|
|
38
|
+
// IPv6 链路本地地址 (fe80::/10)
|
|
39
|
+
{ start: 'fe80::', end: 'febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff' },
|
|
40
|
+
// IPv6 唯一本地地址 (fc00::/7)
|
|
41
|
+
{ start: 'fc00::', end: 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' },
|
|
42
|
+
];
|
|
43
|
+
// 禁止的 Hostname 关键词
|
|
44
|
+
const FORBIDDEN_HOSTNAMES = [
|
|
45
|
+
'localhost',
|
|
46
|
+
'localhost.localdomain',
|
|
47
|
+
'metadata.google.internal', // GCP 元数据服务
|
|
48
|
+
'metadata.google', // GCP 元数据
|
|
49
|
+
'kubernetes.default.svc', // Kubernetes
|
|
50
|
+
'kubernetes.default', // Kubernetes
|
|
51
|
+
'etcd-client.kube-system', // Kubernetes etcd
|
|
52
|
+
'etcd.kube-system', // Kubernetes etcd
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* 将 IP 字符串转换为数字数组(支持 IPv4 和 IPv6)
|
|
56
|
+
*/
|
|
57
|
+
function ipToNumberArray(ip) {
|
|
58
|
+
if (ip.includes(':')) {
|
|
59
|
+
// IPv6
|
|
60
|
+
const parts = ip.split(':');
|
|
61
|
+
return parts.map(part => {
|
|
62
|
+
if (part === '')
|
|
63
|
+
return 0;
|
|
64
|
+
return parseInt(part, 16);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// IPv4
|
|
69
|
+
return ip.split('.').map(part => parseInt(part, 10));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 比较两个 IP 地址(支持 IPv4 和 IPv6)
|
|
74
|
+
* 返回负数表示 a < b,正数表示 a > b,0 表示相等
|
|
75
|
+
*/
|
|
76
|
+
function compareIps(a, b) {
|
|
77
|
+
const aArr = ipToNumberArray(a);
|
|
78
|
+
const bArr = ipToNumberArray(b);
|
|
79
|
+
const maxLen = Math.max(aArr.length, bArr.length);
|
|
80
|
+
for (let i = 0; i < maxLen; i++) {
|
|
81
|
+
const aVal = aArr[i] || 0;
|
|
82
|
+
const bVal = bArr[i] || 0;
|
|
83
|
+
if (aVal !== bVal) {
|
|
84
|
+
return aVal - bVal;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 检查 IP 是否在指定范围内
|
|
91
|
+
*/
|
|
92
|
+
function isIpInRange(ip, start, end) {
|
|
93
|
+
return compareIps(ip, start) >= 0 && compareIps(ip, end) <= 0;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 检测是否为内网 IP
|
|
97
|
+
*
|
|
98
|
+
* @param urlString - 要检查的 URL
|
|
99
|
+
* @returns true 表示是内网 IP
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* 验证 IPv4 地址是否合法(每个八位组在 0-255 之间)
|
|
103
|
+
*/
|
|
104
|
+
function isValidIPv4(ip) {
|
|
105
|
+
const parts = ip.split('.');
|
|
106
|
+
if (parts.length !== 4)
|
|
107
|
+
return false;
|
|
108
|
+
return parts.every(part => {
|
|
109
|
+
const num = parseInt(part, 10);
|
|
110
|
+
return !isNaN(num) && num >= 0 && num <= 255 && String(num) === part;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 验证 IPv6 地址是否合法
|
|
115
|
+
*/
|
|
116
|
+
function isValidIPv6(ip) {
|
|
117
|
+
// 简单验证:检查是否为有效的 IPv6 格式
|
|
118
|
+
if (ip === '::1' || ip === '::')
|
|
119
|
+
return true;
|
|
120
|
+
const parts = ip.split(':');
|
|
121
|
+
// IPv6 应该最多有 8 个部分
|
|
122
|
+
if (parts.length > 8)
|
|
123
|
+
return false;
|
|
124
|
+
return parts.every(part => {
|
|
125
|
+
if (part === '')
|
|
126
|
+
return true;
|
|
127
|
+
// 每个部分应该是 0-4 位十六进制
|
|
128
|
+
return /^[0-9a-fA-F]{1,4}$/.test(part);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function isInternalUrl(urlString) {
|
|
132
|
+
if (!urlString)
|
|
133
|
+
return false;
|
|
134
|
+
try {
|
|
135
|
+
const url = new URL(urlString);
|
|
136
|
+
const hostname = url.hostname.toLowerCase();
|
|
137
|
+
// 检查禁止的 hostname 关键词
|
|
138
|
+
for (const forbidden of FORBIDDEN_HOSTNAMES) {
|
|
139
|
+
if (hostname === forbidden || hostname.endsWith('.' + forbidden)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 检查是否为纯 IP 地址
|
|
144
|
+
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
145
|
+
const ipv6Pattern = /^([0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}$|^::1$|^::$/;
|
|
146
|
+
if (ipPattern.test(hostname)) {
|
|
147
|
+
// 验证 IPv4 是否合法
|
|
148
|
+
if (!isValidIPv4(hostname)) {
|
|
149
|
+
// 无效的 IP 地址,拒绝访问
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
// IPv4
|
|
153
|
+
for (const range of INTERNAL_IP_RANGES) {
|
|
154
|
+
if (range.start.includes('.')) { // IPv4 range
|
|
155
|
+
if (isIpInRange(hostname, range.start, range.end)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (ipv6Pattern.test(hostname) || hostname.includes(':')) {
|
|
162
|
+
// 验证 IPv6 是否合法
|
|
163
|
+
if (!isValidIPv6(hostname)) {
|
|
164
|
+
// 无效的 IP 地址,拒绝访问
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// IPv6
|
|
168
|
+
for (const range of INTERNAL_IP_RANGES) {
|
|
169
|
+
if (range.start.includes(':')) { // IPv6 range
|
|
170
|
+
if (isIpInRange(hostname, range.start, range.end)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* URL 白名单/黑名单验证
|
|
184
|
+
*
|
|
185
|
+
* @param urlString - 要检查的 URL
|
|
186
|
+
* @param whitelist - 白名单域名列表
|
|
187
|
+
* @param blacklist - 黑名单域名列表
|
|
188
|
+
* @returns true 表示允许访问
|
|
189
|
+
*/
|
|
190
|
+
function isAllowedUrl(urlString, whitelist, blacklist) {
|
|
191
|
+
if (!urlString)
|
|
192
|
+
return false;
|
|
193
|
+
try {
|
|
194
|
+
const url = new URL(urlString);
|
|
195
|
+
const hostname = url.hostname.toLowerCase();
|
|
196
|
+
// 先检查黑名单
|
|
197
|
+
if (blacklist && blacklist.length > 0) {
|
|
198
|
+
for (const blocked of blacklist) {
|
|
199
|
+
const blockedLower = blocked.toLowerCase();
|
|
200
|
+
if (hostname === blockedLower || hostname.endsWith('.' + blockedLower)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// 如果有白名单,检查是否在白名单中
|
|
206
|
+
if (whitelist && whitelist.length > 0) {
|
|
207
|
+
for (const allowed of whitelist) {
|
|
208
|
+
const allowedLower = allowed.toLowerCase();
|
|
209
|
+
if (hostname === allowedLower || hostname.endsWith('.' + allowedLower)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 不在白名单中,拒绝访问
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
// 没有白名单,默认允许(但还需要通过内网检查)
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 综合 URL 验证
|
|
225
|
+
* 检查协议、内网 IP、白名单/黑名单
|
|
226
|
+
*
|
|
227
|
+
* @param urlString - 要检查的 URL
|
|
228
|
+
* @param options - 验证选项
|
|
229
|
+
* @returns 验证结果对象
|
|
230
|
+
*/
|
|
231
|
+
function validateUrl(urlString, options = {}) {
|
|
232
|
+
// 安全检查默认不启用
|
|
233
|
+
if (options.enabled !== true) {
|
|
234
|
+
return { valid: true };
|
|
235
|
+
}
|
|
236
|
+
if (!urlString) {
|
|
237
|
+
return { valid: false, error: 'URL 不能为空' };
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const url = new URL(urlString);
|
|
241
|
+
// 协议检查
|
|
242
|
+
const protocol = url.protocol.toLowerCase();
|
|
243
|
+
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
244
|
+
if (!options.allowOtherProtocols) {
|
|
245
|
+
return { valid: false, error: `不支持的协议: ${protocol}` };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// HTTP/HTTPS 显式禁用检查
|
|
249
|
+
if (protocol === 'http:' && options.allowHttp === false) {
|
|
250
|
+
return { valid: false, error: 'HTTP 协议已被禁用' };
|
|
251
|
+
}
|
|
252
|
+
if (protocol === 'https:' && options.allowHttps === false) {
|
|
253
|
+
return { valid: false, error: 'HTTPS 协议已被禁用' };
|
|
254
|
+
}
|
|
255
|
+
// 内网 IP 检查(除非明确允许)
|
|
256
|
+
if (options.allowInternalAccess !== true && isInternalUrl(urlString)) {
|
|
257
|
+
return { valid: false, error: '不允许访问内网 IP 地址' };
|
|
258
|
+
}
|
|
259
|
+
// 白名单/黑名单检查
|
|
260
|
+
if (!isAllowedUrl(urlString, options.whitelist, options.blacklist)) {
|
|
261
|
+
return { valid: false, error: 'URL 不在允许列表中或被列入黑名单' };
|
|
262
|
+
}
|
|
263
|
+
return { valid: true };
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
return { valid: false, error: `URL 解析失败: ${error.message}` };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 验证并抛出异常的便捷函数
|
|
271
|
+
*
|
|
272
|
+
* @param urlString - 要检查的 URL
|
|
273
|
+
* @param options - 验证选项
|
|
274
|
+
* @throws SecurityError 当验证失败时
|
|
275
|
+
*/
|
|
276
|
+
function validateUrlOrThrow(urlString, options = {}) {
|
|
277
|
+
const result = validateUrl(urlString, options);
|
|
278
|
+
if (!result.valid) {
|
|
279
|
+
throw new SecurityError(result.error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 从配置中获取安全验证选项
|
|
284
|
+
*
|
|
285
|
+
* @param config - 插件配置对象
|
|
286
|
+
* @returns 安全验证选项
|
|
287
|
+
*/
|
|
288
|
+
function getSecurityOptions(config) {
|
|
289
|
+
return {
|
|
290
|
+
whitelist: config.security?.whitelist,
|
|
291
|
+
blacklist: config.security?.blacklist,
|
|
292
|
+
allowHttp: config.security?.allowHttp !== false,
|
|
293
|
+
allowHttps: config.security?.allowHttps !== false,
|
|
294
|
+
allowInternalAccess: config.security?.allowInternalAccess === true,
|
|
295
|
+
enabled: config.security?.enabled === true,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 创建带有 URL 验证的 HTTP 函数包装器
|
|
300
|
+
*
|
|
301
|
+
* @param httpFunction - 原始的 HTTP 函数
|
|
302
|
+
* @param config - 配置对象(用于获取白名单/黑名单)
|
|
303
|
+
* @returns 包装后的 HTTP 函数
|
|
304
|
+
*/
|
|
305
|
+
function createSafeHttpFunction(httpFunction, config) {
|
|
306
|
+
const securityOptions = getSecurityOptions(config);
|
|
307
|
+
return async (url, ...args) => {
|
|
308
|
+
// 验证 URL
|
|
309
|
+
validateUrlOrThrow(url, securityOptions);
|
|
310
|
+
return httpFunction(url, ...args);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 观测辅助日志包装层。
|
|
3
|
+
*
|
|
4
|
+
* 注意:当前插件主流程日志入口是 `src/utils/logger.ts` 中的
|
|
5
|
+
* `debug / debugInfo / debugError / createDebugWithContext`。
|
|
6
|
+
* 这里保留为可选的观测与兼容包装,不应作为新业务代码的首选入口。
|
|
4
7
|
*/
|
|
5
8
|
import { Config } from '../types';
|
|
6
9
|
/**
|
|
@@ -62,7 +65,8 @@ export declare class PerformanceTimer {
|
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
/**
|
|
65
|
-
*
|
|
68
|
+
* 结构化日志记录器类。
|
|
69
|
+
* 内部仍然回落到主日志入口,避免形成第二条日志主线。
|
|
66
70
|
*/
|
|
67
71
|
export declare class StructuredLogger {
|
|
68
72
|
private config;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 观测辅助日志包装层。
|
|
4
|
+
*
|
|
5
|
+
* 注意:当前插件主流程日志入口是 `src/utils/logger.ts` 中的
|
|
6
|
+
* `debug / debugInfo / debugError / createDebugWithContext`。
|
|
7
|
+
* 这里保留为可选的观测与兼容包装,不应作为新业务代码的首选入口。
|
|
5
8
|
*/
|
|
6
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
10
|
exports.StructuredLogger = exports.PerformanceTimer = exports.LogLevel = void 0;
|
|
@@ -12,9 +15,7 @@ exports.logDetails = logDetails;
|
|
|
12
15
|
exports.logError = logError;
|
|
13
16
|
exports.createTimer = createTimer;
|
|
14
17
|
exports.logPerformance = logPerformance;
|
|
15
|
-
const
|
|
16
|
-
const types_1 = require("../types");
|
|
17
|
-
const logger = new koishi_1.Logger('rss-owl');
|
|
18
|
+
const logger_1 = require("./logger");
|
|
18
19
|
/**
|
|
19
20
|
* 日志级别枚举
|
|
20
21
|
*/
|
|
@@ -60,7 +61,8 @@ class PerformanceTimer {
|
|
|
60
61
|
}
|
|
61
62
|
exports.PerformanceTimer = PerformanceTimer;
|
|
62
63
|
/**
|
|
63
|
-
*
|
|
64
|
+
* 结构化日志记录器类。
|
|
65
|
+
* 内部仍然回落到主日志入口,避免形成第二条日志主线。
|
|
64
66
|
*/
|
|
65
67
|
class StructuredLogger {
|
|
66
68
|
config;
|
|
@@ -73,11 +75,7 @@ class StructuredLogger {
|
|
|
73
75
|
* 判断是否应该记录日志
|
|
74
76
|
*/
|
|
75
77
|
shouldLog(level) {
|
|
76
|
-
|
|
77
|
-
if (typeLevel < 1)
|
|
78
|
-
return false;
|
|
79
|
-
const configLevel = types_1.debugLevel.findIndex(i => i === this.config.debug);
|
|
80
|
-
return typeLevel <= configLevel;
|
|
78
|
+
return (0, logger_1.shouldLog)(this.config, level);
|
|
81
79
|
}
|
|
82
80
|
/**
|
|
83
81
|
* 格式化日志条目
|
|
@@ -86,38 +84,41 @@ class StructuredLogger {
|
|
|
86
84
|
if (this.enableJsonOutput) {
|
|
87
85
|
return JSON.stringify(entry);
|
|
88
86
|
}
|
|
89
|
-
|
|
90
|
-
const parts = [
|
|
87
|
+
const header = [
|
|
91
88
|
`[${entry.timestamp}]`,
|
|
92
89
|
`[${entry.level.toUpperCase()}]`,
|
|
93
90
|
entry.module ? `[${entry.module}]` : '',
|
|
94
91
|
entry.message
|
|
95
92
|
].filter(Boolean).join(' ');
|
|
96
|
-
|
|
93
|
+
const lines = [header];
|
|
94
|
+
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
95
|
+
const contextStr = Object.entries(entry.context)
|
|
96
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
97
|
+
.join(', ');
|
|
98
|
+
lines.push(`↳ context: ${contextStr}`);
|
|
99
|
+
}
|
|
100
|
+
if (entry.data && Object.keys(entry.data).length > 0) {
|
|
101
|
+
const dataStr = Object.entries(entry.data)
|
|
102
|
+
.map(([key, value]) => `${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`)
|
|
103
|
+
.join(', ');
|
|
104
|
+
lines.push(`↳ data: ${dataStr}`);
|
|
105
|
+
}
|
|
97
106
|
if (entry.performance) {
|
|
98
107
|
const perfParts = [];
|
|
99
108
|
if (entry.performance.duration) {
|
|
100
|
-
perfParts.push(
|
|
109
|
+
perfParts.push(`duration=${entry.performance.duration}ms`);
|
|
101
110
|
}
|
|
102
111
|
if (entry.performance.memory) {
|
|
103
|
-
perfParts.push(
|
|
112
|
+
perfParts.push(`memory=${entry.performance.memory.toFixed(2)}MB`);
|
|
104
113
|
}
|
|
105
114
|
if (perfParts.length > 0) {
|
|
106
|
-
|
|
115
|
+
lines.push(`↳ performance: ${perfParts.join(', ')}`);
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
|
-
// 添加上下文数据
|
|
110
|
-
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
111
|
-
const contextStr = Object.entries(entry.context)
|
|
112
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
113
|
-
.join(', ');
|
|
114
|
-
return parts + `\n📍 ${contextStr}`;
|
|
115
|
-
}
|
|
116
|
-
// 添加错误信息
|
|
117
118
|
if (entry.error) {
|
|
118
|
-
|
|
119
|
+
lines.push(`↳ error: ${entry.error.name}: ${entry.error.message}`);
|
|
119
120
|
}
|
|
120
|
-
return
|
|
121
|
+
return lines.join('\n');
|
|
121
122
|
}
|
|
122
123
|
/**
|
|
123
124
|
* 记录日志
|
|
@@ -127,18 +128,7 @@ class StructuredLogger {
|
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
const formattedMessage = this.formatEntry(entry);
|
|
130
|
-
|
|
131
|
-
switch (entry.level) {
|
|
132
|
-
case LogLevel.ERROR:
|
|
133
|
-
logger.error(formattedMessage);
|
|
134
|
-
break;
|
|
135
|
-
case LogLevel.INFO:
|
|
136
|
-
case LogLevel.DETAILS:
|
|
137
|
-
logger.info(formattedMessage);
|
|
138
|
-
break;
|
|
139
|
-
case LogLevel.DISABLE:
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
131
|
+
(0, logger_1.debug)(this.config, formattedMessage, '', entry.level);
|
|
142
132
|
}
|
|
143
133
|
/**
|
|
144
134
|
* 记录普通信息
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anyul/koishi-plugin-rss",
|
|
3
3
|
"description": "Koishi RSS订阅器,支持多种RSS源、图片渲染、AI摘要等高级功能",
|
|
4
|
-
"version": "5.2.
|
|
4
|
+
"version": "5.2.4",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"author": "Anyuluo <anyul@email.com>",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"axios": "^1.13.2",
|
|
37
37
|
"cheerio": "^1.1.2",
|
|
38
38
|
"https-proxy-agent": "^7.0.6",
|
|
39
|
+
"isomorphic-dompurify": "^2.14.0",
|
|
39
40
|
"koishi": "^4.18.10",
|
|
40
41
|
"koishi-plugin-puppeteer": "^3.9.0",
|
|
41
42
|
"marked": "^4.3.0",
|