@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.
Files changed (90) hide show
  1. package/README.md +92 -37
  2. package/lib/commands/error-handler.js +13 -4
  3. package/lib/commands/index.d.ts +20 -1
  4. package/lib/commands/index.js +394 -2
  5. package/lib/commands/runtime.d.ts +17 -0
  6. package/lib/commands/runtime.js +27 -0
  7. package/lib/commands/subscription-create.d.ts +23 -0
  8. package/lib/commands/subscription-create.js +145 -0
  9. package/lib/commands/subscription-edit.d.ts +7 -0
  10. package/lib/commands/subscription-edit.js +177 -0
  11. package/lib/commands/subscription-management.d.ts +12 -0
  12. package/lib/commands/subscription-management.js +176 -0
  13. package/lib/commands/utils.d.ts +13 -1
  14. package/lib/commands/utils.js +43 -2
  15. package/lib/commands/web-monitor.d.ts +15 -0
  16. package/lib/commands/web-monitor.js +222 -0
  17. package/lib/config.js +25 -0
  18. package/lib/constants.d.ts +1 -1
  19. package/lib/constants.js +46 -83
  20. package/lib/core/ai-cache.d.ts +27 -0
  21. package/lib/core/ai-cache.js +169 -0
  22. package/lib/core/ai-client.d.ts +12 -0
  23. package/lib/core/ai-client.js +65 -0
  24. package/lib/core/ai-selector.d.ts +2 -0
  25. package/lib/core/ai-selector.js +80 -0
  26. package/lib/core/ai-summary.d.ts +10 -0
  27. package/lib/core/ai-summary.js +73 -0
  28. package/lib/core/ai-utils.d.ts +10 -0
  29. package/lib/core/ai-utils.js +104 -0
  30. package/lib/core/ai.d.ts +3 -77
  31. package/lib/core/ai.js +13 -455
  32. package/lib/core/feeder-arg.d.ts +17 -0
  33. package/lib/core/feeder-arg.js +234 -0
  34. package/lib/core/feeder-runtime.d.ts +96 -0
  35. package/lib/core/feeder-runtime.js +233 -0
  36. package/lib/core/feeder.d.ts +4 -6
  37. package/lib/core/feeder.js +120 -304
  38. package/lib/core/item-processor-runtime.d.ts +46 -0
  39. package/lib/core/item-processor-runtime.js +215 -0
  40. package/lib/core/item-processor-template.d.ts +16 -0
  41. package/lib/core/item-processor-template.js +158 -0
  42. package/lib/core/item-processor.d.ts +1 -10
  43. package/lib/core/item-processor.js +48 -393
  44. package/lib/core/notification-queue-retry.d.ts +25 -0
  45. package/lib/core/notification-queue-retry.js +78 -0
  46. package/lib/core/notification-queue-sender.d.ts +20 -0
  47. package/lib/core/notification-queue-sender.js +118 -0
  48. package/lib/core/notification-queue-store.d.ts +19 -0
  49. package/lib/core/notification-queue-store.js +137 -0
  50. package/lib/core/notification-queue-types.d.ts +49 -0
  51. package/lib/core/notification-queue-types.js +2 -0
  52. package/lib/core/notification-queue.d.ts +13 -72
  53. package/lib/core/notification-queue.js +132 -262
  54. package/lib/core/parser.js +12 -0
  55. package/lib/core/renderer.d.ts +15 -0
  56. package/lib/core/renderer.js +91 -23
  57. package/lib/core/search-format.d.ts +3 -0
  58. package/lib/core/search-format.js +36 -0
  59. package/lib/core/search-providers.d.ts +13 -0
  60. package/lib/core/search-providers.js +175 -0
  61. package/lib/core/search-rotation.d.ts +4 -0
  62. package/lib/core/search-rotation.js +55 -0
  63. package/lib/core/search-service.d.ts +3 -0
  64. package/lib/core/search-service.js +100 -0
  65. package/lib/core/search-types.d.ts +39 -0
  66. package/lib/core/search-types.js +2 -0
  67. package/lib/core/search.d.ts +4 -101
  68. package/lib/core/search.js +10 -508
  69. package/lib/index.js +50 -1160
  70. package/lib/tsconfig.tsbuildinfo +1 -1
  71. package/lib/types.d.ts +51 -6
  72. package/lib/utils/common.js +52 -3
  73. package/lib/utils/error-handler.d.ts +8 -0
  74. package/lib/utils/error-handler.js +27 -0
  75. package/lib/utils/error-tracker.js +24 -8
  76. package/lib/utils/fetcher.js +68 -9
  77. package/lib/utils/legacy-config.d.ts +12 -0
  78. package/lib/utils/legacy-config.js +56 -0
  79. package/lib/utils/logger.d.ts +4 -2
  80. package/lib/utils/logger.js +193 -34
  81. package/lib/utils/media.js +3 -6
  82. package/lib/utils/proxy.d.ts +3 -0
  83. package/lib/utils/proxy.js +14 -0
  84. package/lib/utils/sanitizer.d.ts +58 -0
  85. package/lib/utils/sanitizer.js +227 -0
  86. package/lib/utils/security.d.ts +75 -0
  87. package/lib/utils/security.js +312 -0
  88. package/lib/utils/structured-logger.d.ts +7 -3
  89. package/lib/utils/structured-logger.js +29 -39
  90. 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
- * 提供统一的日志格式,支持 JSON 输出和性能监控
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
- * 提供统一的日志格式,支持 JSON 输出和性能监控
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 koishi_1 = require("koishi");
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
- const typeLevel = types_1.debugLevel.findIndex(i => i === level);
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(`⏱️ ${entry.performance.duration}ms`);
109
+ perfParts.push(`duration=${entry.performance.duration}ms`);
101
110
  }
102
111
  if (entry.performance.memory) {
103
- perfParts.push(`💾 ${entry.performance.memory.toFixed(2)}MB`);
112
+ perfParts.push(`memory=${entry.performance.memory.toFixed(2)}MB`);
104
113
  }
105
114
  if (perfParts.length > 0) {
106
- return parts + '\n' + perfParts.join(' | ');
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
- return parts + `\n❌ ${entry.error.name}: ${entry.error.message}`;
119
+ lines.push(`↳ error: ${entry.error.name}: ${entry.error.message}`);
119
120
  }
120
- return parts;
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.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",