@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
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logger = void 0;
4
+ exports.applyDebugLevel = applyDebugLevel;
4
5
  exports.shouldLog = shouldLog;
5
6
  exports.debug = debug;
6
7
  exports.debugError = debugError;
@@ -10,6 +11,47 @@ const koishi_1 = require("koishi");
10
11
  const types_1 = require("../types");
11
12
  const logger = new koishi_1.Logger('rss-owl');
12
13
  exports.logger = logger;
14
+ /**
15
+ * 把 config.debug 映射到 Koishi/reggol 的原生日志级别数值。
16
+ *
17
+ * reggol 级别(见 reggol/index.d.ts):
18
+ * SILENT=0, SUCCESS/ERROR=1, INFO/WARN=2, DEBUG=3
19
+ *
20
+ * 本插件的 debug 字段语义:
21
+ * disable → 0(SILENT,什么也不输出)
22
+ * error → 1(仅 error)
23
+ * info → 2(error + info/warn)
24
+ * details → 3(含 debug 全量)
25
+ */
26
+ function debugLevelToReggol(debug) {
27
+ switch (debug) {
28
+ case 'error': return koishi_1.Logger.ERROR;
29
+ case 'info': return koishi_1.Logger.INFO;
30
+ case 'details': return koishi_1.Logger.DEBUG;
31
+ default: return koishi_1.Logger.SILENT;
32
+ }
33
+ }
34
+ /**
35
+ * 把 config.debug 同步到 Koishi 原生日志分级 `Logger.levels['rss-owl']`。
36
+ *
37
+ * 这样本插件的日志可见性既受自身 debug 字段控制,也能被 Koishi 全局 levels
38
+ * (WebUI 的 logger 插件、配置文件中的 levels.base / levels['rss-owl'])覆盖/调节。
39
+ * 应在 apply() 启动时调用一次;值未变化时不重复写入。
40
+ */
41
+ let lastAppliedLevel;
42
+ function applyDebugLevel(config) {
43
+ const target = debugLevelToReggol(config.debug);
44
+ if (lastAppliedLevel === target)
45
+ return;
46
+ lastAppliedLevel = target;
47
+ koishi_1.Logger.levels = koishi_1.Logger.levels || { base: 2 };
48
+ koishi_1.Logger.levels['rss-owl'] = target;
49
+ }
50
+ /**
51
+ * 按 config.debug 判定某级是否应输出。
52
+ * 在 debug() 中作为性能预检门控(避免对注定被丢弃的日志做脱敏/格式化),
53
+ * 与 applyDebugLevel 同步到 Koishi levels 的语义保持一致;最终输出与否仍由 reggol levels 决定。
54
+ */
13
55
  function shouldLog(config, type) {
14
56
  const typeLevel = types_1.debugLevel.findIndex(i => i === type);
15
57
  if (typeLevel < 1)
@@ -19,6 +61,22 @@ function shouldLog(config, type) {
19
61
  return false;
20
62
  return typeLevel <= configLevel;
21
63
  }
64
+ /**
65
+ * 子命名空间 logger 缓存,避免每条日志都 extend。
66
+ * 用 logger.extend('feeder') 产出 rss-owl:feeder,命名空间天然带模块名,
67
+ * 取代此前 formatTextLog 里手工拼 [name] 前缀的做法(与 Koishi Logger 命名空间重复)。
68
+ */
69
+ const subLoggers = new Map();
70
+ function getSubLogger(name) {
71
+ if (!name)
72
+ return logger;
73
+ let sub = subLoggers.get(name);
74
+ if (!sub) {
75
+ sub = logger.extend(name);
76
+ subLoggers.set(name, sub);
77
+ }
78
+ return sub;
79
+ }
22
80
  /**
23
81
  * 敏感信息模式定义
24
82
  */
@@ -129,12 +187,21 @@ function sanitizeObject(obj, depth = 0) {
129
187
  }
130
188
  return obj;
131
189
  }
132
- function emitLog(type, content) {
133
- if (type === 'error') {
134
- logger.error(content);
135
- return;
136
- }
137
- logger.info(content);
190
+ /**
191
+ * Koishi 原生日志级别输出。
192
+ *
193
+ * 关键纠错:'details' 之前被错压成 logger.info(),现归到 logger.debug(),
194
+ * 让 reggol 的 levels(SILENT=0 / ERROR=1 / INFO=2 / DEBUG=3)能正确分级过滤。
195
+ * 模块名 name 通过子命名空间 logger.extend(name) 体现(如 rss-owl:feeder),
196
+ * 不再手工拼 [name] 前缀。
197
+ */
198
+ function emitLog(type, content, name = '') {
199
+ const target = getSubLogger(name);
200
+ if (type === 'error')
201
+ return target.error(content);
202
+ if (type === 'info')
203
+ return target.info(content);
204
+ return target.debug(content); // 'details'
138
205
  }
139
206
  function filterContextFields(context, contextFields) {
140
207
  if (!contextFields?.length) {
@@ -163,12 +230,10 @@ function formatContextValue(value) {
163
230
  return String(value);
164
231
  }
165
232
  function formatTextLog(message, name, context, loggingConfig) {
166
- const parts = [];
167
- if (loggingConfig?.includeModule !== false && name) {
168
- parts.push(`[${name}]`);
169
- }
170
- parts.push(message);
171
- const textOutput = parts.filter(Boolean).join(' ').trim();
233
+ // 模块名不再手工拼前缀:已由子命名空间 logger(rss-owl:<name>)体现,
234
+ // 避免与 Koishi Logger 命名空间重复输出。
235
+ void name;
236
+ const textOutput = message.trim();
172
237
  if (!context || Object.keys(context).length === 0) {
173
238
  return textOutput;
174
239
  }
@@ -192,6 +257,9 @@ function formatTextLog(message, name, context, loggingConfig) {
192
257
  * @param context - 额外的上下文信息
193
258
  */
194
259
  function debug(config, message, name = '', type = 'details', context) {
260
+ // 性能预检:按 config.debug 快速跳过注定被 Koishi levels 丢弃的日志,
261
+ // 避免无谓的脱敏/格式化开销。最终是否真正输出由 reggol levels 决定
262
+ // (applyDebugLevel 已把 config.debug 同步到 Logger.levels['rss-owl'])。
195
263
  if (!shouldLog(config, type))
196
264
  return;
197
265
  // 检查是否启用日志脱敏(默认启用)
@@ -258,10 +326,10 @@ function debug(config, message, name = '', type = 'details', context) {
258
326
  }
259
327
  }
260
328
  // 输出结构化日志
261
- emitLog(type, JSON.stringify(logEntry));
329
+ emitLog(type, JSON.stringify(logEntry), name);
262
330
  }
263
331
  else {
264
- emitLog(type, formatTextLog(formattedMessage, name, context, loggingConfig));
332
+ emitLog(type, formatTextLog(formattedMessage, name, context, loggingConfig), name);
265
333
  }
266
334
  }
267
335
  /**
@@ -50,10 +50,10 @@ async function tryUnlink(filePath) {
50
50
  }
51
51
  }
52
52
  const getCacheDir = (config) => {
53
- let dir = config.basic.cacheDir ? path.resolve('./', config.basic.cacheDir || "") : `${__dirname}/cache`;
54
- let mkdir = (path, deep = 2) => {
55
- let dir = path.split("\\").splice(0, deep).join("\\");
56
- let dirDeep = path.split("\\").length;
53
+ const dir = config.basic.cacheDir ? path.resolve('./', config.basic.cacheDir || "") : `${__dirname}/cache`;
54
+ const mkdir = (path, deep = 2) => {
55
+ const dir = path.split("\\").splice(0, deep).join("\\");
56
+ const dirDeep = path.split("\\").length;
57
57
  if (!fs.existsSync(dir)) {
58
58
  fs.mkdirSync(dir);
59
59
  }
@@ -68,11 +68,11 @@ exports.getCacheDir = getCacheDir;
68
68
  const writeCacheFile = async (fileUrl, config) => {
69
69
  const cacheDir = (0, exports.getCacheDir)(config);
70
70
  (0, logger_1.debug)(config, cacheDir, 'cacheDir', 'details');
71
- let suffix = /(?<=^data:.+?\/).+?(?=;base64)/.exec(fileUrl)?.[0] || 'bin';
71
+ const suffix = /(?<=^data:.+?\/).+?(?=;base64)/.exec(fileUrl)?.[0] || 'bin';
72
72
  // 使用时间戳 + 随机数生成唯一文件名,避免竞态条件
73
- let fileName = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}.${suffix}`;
74
- let base64Data = fileUrl.replace(/^data:.+?;base64,/, "");
75
- let filePath = `${cacheDir}/${fileName}`;
73
+ const fileName = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}.${suffix}`;
74
+ const base64Data = fileUrl.replace(/^data:.+?;base64,/, "");
75
+ const filePath = `${cacheDir}/${fileName}`;
76
76
  fs.writeFileSync(filePath, base64Data, 'base64');
77
77
  if (config.basic.replaceDir) {
78
78
  return `file:///${config.basic.replaceDir}/${fileName}`;
@@ -120,10 +120,10 @@ const getImageUrl = async (ctx, config, $http, url, arg, useBase64Mode = false)
120
120
  (0, logger_1.debug)(config, `图片请求失败: ${error}`, 'img error', 'error');
121
121
  return '';
122
122
  }
123
- let contentType = res.headers["content-type"] || 'image/jpeg';
124
- let suffix = contentType?.split('/')[1] || 'jpg';
125
- let base64Prefix = `data:${contentType};base64,`;
126
- let base64Data = base64Prefix + Buffer.from(res.data, 'binary').toString('base64');
123
+ const contentType = res.headers["content-type"] || 'image/jpeg';
124
+ const suffix = contentType?.split('/')[1] || 'jpg';
125
+ const base64Prefix = `data:${contentType};base64,`;
126
+ const base64Data = base64Prefix + Buffer.from(res.data, 'binary').toString('base64');
127
127
  // 根据发送模式处理
128
128
  const imageMode = config.basic.imageMode;
129
129
  // base64 模式:直接返回 base64
@@ -132,13 +132,13 @@ const getImageUrl = async (ctx, config, $http, url, arg, useBase64Mode = false)
132
132
  }
133
133
  // File 模式:下载到本地,返回 file:// URL
134
134
  if (imageMode == 'File') {
135
- let fileUrl = await (0, exports.writeCacheFile)(base64Data, config);
135
+ const fileUrl = await (0, exports.writeCacheFile)(base64Data, config);
136
136
  return fileUrl;
137
137
  }
138
138
  // assets 模式:下载到本地,上传到 assets,返回 assets URL
139
139
  if (imageMode === 'assets' && ctx.assets) {
140
140
  try {
141
- let assetUrl = await ctx.assets.upload(base64Data, `rss-img-${Date.now()}.${suffix}`);
141
+ const assetUrl = await ctx.assets.upload(base64Data, `rss-img-${Date.now()}.${suffix}`);
142
142
  (0, logger_1.debug)(config, `图片 Assets 上传成功: ${assetUrl}`, 'assets', 'info');
143
143
  return assetUrl;
144
144
  }
@@ -152,7 +152,7 @@ const getImageUrl = async (ctx, config, $http, url, arg, useBase64Mode = false)
152
152
  };
153
153
  exports.getImageUrl = getImageUrl;
154
154
  const getVideoUrl = async (ctx, config, $http, url, arg, useBase64Mode = false, dom) => {
155
- let src = dom.attribs.src || dom.children["0"].attribs.src;
155
+ const src = dom.attribs.src || dom.children["0"].attribs.src;
156
156
  // 根据发送模式处理
157
157
  const videoMode = config.basic.videoMode;
158
158
  // filter 模式:过滤掉所有视频
@@ -222,23 +222,23 @@ const getVideoUrl = async (ctx, config, $http, url, arg, useBase64Mode = false,
222
222
  // file:// 已读入内存(bufferData),后续 base64/File/assets 都基于内存数据,
223
223
  // 源文件不再需要 —— 立即删,避免几十 MB 视频在缓存目录堆积
224
224
  await tryUnlink(localSourcePath);
225
- let suffix = contentType?.split('/')[1] || 'mp4';
226
- let base64Prefix = `data:${contentType};base64,`;
227
- let base64Data = base64Prefix + bufferData.toString('base64');
225
+ const suffix = contentType?.split('/')[1] || 'mp4';
226
+ const base64Prefix = `data:${contentType};base64,`;
227
+ const base64Data = base64Prefix + bufferData.toString('base64');
228
228
  // base64 模式:直接返回 base64(注意:视频 base64 可能非常长)
229
229
  if (videoMode === 'base64' || useBase64Mode) {
230
230
  return base64Data;
231
231
  }
232
232
  // File 模式:下载到本地,返回 file:// URL
233
233
  if (videoMode === 'File') {
234
- let fileUrl = await (0, exports.writeCacheFile)(base64Data, config);
234
+ const fileUrl = await (0, exports.writeCacheFile)(base64Data, config);
235
235
  return fileUrl;
236
236
  }
237
237
  // assets 模式:下载到本地,上传到 assets,返回 assets URL
238
238
  if (videoMode === 'assets' && ctx.assets) {
239
239
  try {
240
240
  // 注意:大型视频的 base64 字符串可能很长,某些 assets 插件可能处理较慢
241
- let assetUrl = await ctx.assets.upload(base64Data, `rss-video-${Date.now()}.${suffix}`);
241
+ const assetUrl = await ctx.assets.upload(base64Data, `rss-video-${Date.now()}.${suffix}`);
242
242
  (0, logger_1.debug)(config, `视频 Assets 上传成功: ${assetUrl}`, 'assets', 'info');
243
243
  return assetUrl;
244
244
  }
@@ -254,7 +254,7 @@ exports.getVideoUrl = getVideoUrl;
254
254
  const puppeteerToFile = async (ctx, config, puppeteer) => {
255
255
  // puppeteer.render() 返回 Element 字符串,格式如: <img src="data:image/png;base64,..."/> 或 <img src="https://..."/>
256
256
  // 提取 src 属性
257
- let base64 = /(?<=src=").+?(?=")/.exec(puppeteer)?.[0];
257
+ const base64 = /(?<=src=").+?(?=")/.exec(puppeteer)?.[0];
258
258
  if (!base64) {
259
259
  (0, logger_1.debug)(config, `puppeteer render 返回值格式异常: ${puppeteer}`, 'puppeteerToFile', 'error');
260
260
  return puppeteer;
@@ -124,3 +124,11 @@ export declare function initMessageCache(ctx: Context, config: Config, maxSize?:
124
124
  * 获取全局消息缓存管理器
125
125
  */
126
126
  export declare function getMessageCache(): MessageCacheManager | null;
127
+ /**
128
+ * 销毁全局消息缓存管理器
129
+ *
130
+ * 必须在插件 dispose 时调用:MessageCacheManager 构造时持有了 ctx,
131
+ * 若热重载后仍复用旧实例,会对已 dispose 的 ctx 执行数据库操作而报错/静默丢失。
132
+ * 置 null 后,下次 initMessageCache 会用新 ctx 重建实例。
133
+ */
134
+ export declare function disposeMessageCache(): void;
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.MessageCacheManager = void 0;
8
8
  exports.initMessageCache = initMessageCache;
9
9
  exports.getMessageCache = getMessageCache;
10
+ exports.disposeMessageCache = disposeMessageCache;
10
11
  const koishi_1 = require("koishi");
11
12
  const logger_1 = require("./logger");
12
13
  const logger = new koishi_1.Logger('rss-owl:cache');
@@ -253,3 +254,14 @@ function initMessageCache(ctx, config, maxSize = 100) {
253
254
  function getMessageCache() {
254
255
  return globalMessageCache;
255
256
  }
257
+ /**
258
+ * 销毁全局消息缓存管理器
259
+ *
260
+ * 必须在插件 dispose 时调用:MessageCacheManager 构造时持有了 ctx,
261
+ * 若热重载后仍复用旧实例,会对已 dispose 的 ctx 执行数据库操作而报错/静默丢失。
262
+ * 置 null 后,下次 initMessageCache 会用新 ctx 重建实例。
263
+ */
264
+ function disposeMessageCache() {
265
+ globalMessageCache = null;
266
+ logger.info('消息缓存管理器已销毁');
267
+ }
@@ -1,3 +1,40 @@
1
1
  import type { AxiosRequestConfig } from 'axios';
2
2
  import type { Config } from '../types';
3
+ /**
4
+ * 代理配置片段(订阅级 / 全局级通用形状)。
5
+ */
6
+ export interface ProxyAgentShape {
7
+ enabled?: boolean;
8
+ protocol?: string;
9
+ host?: string;
10
+ port?: number | string;
11
+ auth?: {
12
+ enabled?: boolean;
13
+ user?: string;
14
+ pass?: string;
15
+ } & Record<string, any>;
16
+ [key: string]: any;
17
+ }
18
+ /**
19
+ * 合并订阅级代理配置 (argProxy) 与全局代理配置 (configProxy)。
20
+ *
21
+ * 优先级:
22
+ * 1. argProxy.enabled === false → 显式禁用
23
+ * 2. argProxy.enabled === true 且有 host → 用订阅配置
24
+ * 3. argProxy 缺失 / 未设置 enabled → 回落全局配置
25
+ * 4. argProxy.enabled === true 但缺 host → 用全局补全
26
+ * 5. 其余 → 禁用
27
+ *
28
+ * 返回值与 `config.net.proxyAgent` 同构,可直接喂给 `buildAxiosProxyConfig`。
29
+ *
30
+ * 本函数原位于 `core/feeder-arg.ts`,提升到此处以统一代理 helper 归属,
31
+ * 供 fetcher / feeder 等模块复用,避免重复实现合并逻辑。
32
+ */
33
+ export declare function mergeProxyAgent(argProxy: any, configProxy: any, config: Config): ProxyAgentShape;
34
+ /**
35
+ * 构造 axios 的代理配置(httpsAgent + 关闭 axios 原生 proxy)。
36
+ *
37
+ * 接受完整的 Config(读取 config.net.proxyAgent)。对于需要按订阅合并代理的场景,
38
+ * 先用 `mergeProxyAgent` 得到统一片段,再包成 `{ net: { proxyAgent } }` 传入。
39
+ */
3
40
  export declare function buildAxiosProxyConfig(config: Config): Pick<AxiosRequestConfig, 'httpsAgent' | 'proxy'>;
@@ -1,7 +1,67 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeProxyAgent = mergeProxyAgent;
3
4
  exports.buildAxiosProxyConfig = buildAxiosProxyConfig;
4
5
  const https_proxy_agent_1 = require("https-proxy-agent");
6
+ const logger_1 = require("./logger");
7
+ /**
8
+ * 合并订阅级代理配置 (argProxy) 与全局代理配置 (configProxy)。
9
+ *
10
+ * 优先级:
11
+ * 1. argProxy.enabled === false → 显式禁用
12
+ * 2. argProxy.enabled === true 且有 host → 用订阅配置
13
+ * 3. argProxy 缺失 / 未设置 enabled → 回落全局配置
14
+ * 4. argProxy.enabled === true 但缺 host → 用全局补全
15
+ * 5. 其余 → 禁用
16
+ *
17
+ * 返回值与 `config.net.proxyAgent` 同构,可直接喂给 `buildAxiosProxyConfig`。
18
+ *
19
+ * 本函数原位于 `core/feeder-arg.ts`,提升到此处以统一代理 helper 归属,
20
+ * 供 fetcher / feeder 等模块复用,避免重复实现合并逻辑。
21
+ */
22
+ function mergeProxyAgent(argProxy, configProxy, config) {
23
+ (0, logger_1.debug)(config, `合并代理配置 - argProxy: ${JSON.stringify(argProxy)}, configProxy.enabled: ${configProxy?.enabled}`, 'proxy merge debug', 'details');
24
+ if (argProxy?.enabled === false) {
25
+ (0, logger_1.debug)(config, '订阅明确禁用代理', 'proxy merge', 'details');
26
+ return { enabled: false };
27
+ }
28
+ if (argProxy?.enabled === true && argProxy?.host) {
29
+ (0, logger_1.debug)(config, '使用订阅的代理配置', 'proxy merge', 'details');
30
+ return argProxy;
31
+ }
32
+ const shouldUseConfigProxy = !argProxy || Object.keys(argProxy || {}).length === 0 || argProxy?.enabled === undefined || argProxy?.enabled === null;
33
+ if (shouldUseConfigProxy) {
34
+ if (configProxy?.enabled) {
35
+ const result = {
36
+ enabled: true,
37
+ protocol: configProxy.protocol,
38
+ host: configProxy.host,
39
+ port: configProxy.port,
40
+ auth: configProxy.auth?.enabled ? configProxy.auth : undefined,
41
+ };
42
+ (0, logger_1.debug)(config, `使用全局代理: ${result.protocol}://${result.host}:${result.port}`, 'proxy merge', 'info');
43
+ return result;
44
+ }
45
+ (0, logger_1.debug)(config, '全局代理未启用', 'proxy merge', 'details');
46
+ }
47
+ if (argProxy?.enabled === true && !argProxy?.host) {
48
+ const result = {
49
+ ...configProxy,
50
+ ...argProxy,
51
+ auth: configProxy?.auth?.enabled ? configProxy.auth : undefined,
52
+ };
53
+ (0, logger_1.debug)(config, '订阅代理配置不完整,补充全局配置', 'proxy merge', 'details');
54
+ return result;
55
+ }
56
+ (0, logger_1.debug)(config, '代理未配置,使用默认(禁用)', 'proxy merge', 'details');
57
+ return { enabled: false };
58
+ }
59
+ /**
60
+ * 构造 axios 的代理配置(httpsAgent + 关闭 axios 原生 proxy)。
61
+ *
62
+ * 接受完整的 Config(读取 config.net.proxyAgent)。对于需要按订阅合并代理的场景,
63
+ * 先用 `mergeProxyAgent` 得到统一片段,再包成 `{ net: { proxyAgent } }` 传入。
64
+ */
5
65
  function buildAxiosProxyConfig(config) {
6
66
  if (!config.net?.proxyAgent?.enabled) {
7
67
  return {};
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@anyul/koishi-plugin-rss",
3
3
  "description": "Koishi RSS订阅器,支持多种RSS源、图片渲染、AI摘要等高级功能",
4
- "version": "5.3.2",
4
+ "version": "5.3.10",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
+ "packageManager": "pnpm@10.12.1",
7
8
  "author": "Anyuluo <anyul@email.com>",
8
9
  "contributors": [],
9
10
  "repository": {
@@ -43,16 +44,28 @@
43
44
  "x2js": "^3.4.4"
44
45
  },
45
46
  "devDependencies": {
47
+ "@eslint/js": "^8.57.1",
46
48
  "@jest/globals": "^30.2.0",
47
49
  "@types/jest": "^30.0.0",
50
+ "@types/node": "^26.0.1",
51
+ "eslint": "^9.39.4",
52
+ "globals": "^15.15.0",
48
53
  "jest": "^30.2.0",
49
54
  "ts-jest": "^29.4.6",
50
- "typescript": "^5.9.3"
55
+ "typescript": "^5.9.3",
56
+ "typescript-eslint": "^8.62.0"
51
57
  },
52
58
  "scripts": {
53
59
  "test": "jest",
54
60
  "test:watch": "jest --watch",
55
61
  "test:coverage": "jest --coverage",
56
- "build": "tsc"
62
+ "test:ci": "jest --ci --reporters=default",
63
+ "build": "tsc",
64
+ "typecheck": "tsc --noEmit",
65
+ "lint": "eslint src tests",
66
+ "lint:fix": "eslint src tests --fix"
67
+ },
68
+ "engines": {
69
+ "node": ">=20"
57
70
  }
58
71
  }
@@ -1,53 +0,0 @@
1
- /**
2
- * 命令错误处理中间件
3
- * 提供统一的错误处理和用户友好的错误消息
4
- */
5
- import { Context } from 'koishi';
6
- import { Config } from '../types';
7
- /**
8
- * 命令执行结果
9
- */
10
- export interface CommandResult {
11
- success: boolean;
12
- message: string;
13
- error?: any;
14
- }
15
- /**
16
- * 命令错误类型
17
- */
18
- export declare enum CommandErrorType {
19
- PERMISSION_DENIED = "PERMISSION_DENIED",
20
- INVALID_ARGUMENT = "INVALID_ARGUMENT",
21
- NOT_FOUND = "NOT_FOUND",
22
- ALREADY_EXISTS = "ALREADY_EXISTS",
23
- NETWORK_ERROR = "NETWORK_ERROR",
24
- INTERNAL_ERROR = "INTERNAL_ERROR"
25
- }
26
- /**
27
- * 命令错误类
28
- */
29
- export declare class CommandError extends Error {
30
- type: CommandErrorType;
31
- details?: any;
32
- constructor(type: CommandErrorType, message: string, details?: any);
33
- }
34
- /**
35
- * 包装命令执行,提供统一的错误处理
36
- */
37
- export declare function executeCommand(ctx: Context, config: Config, operationName: string, handler: () => Promise<string>): Promise<string>;
38
- /**
39
- * 创建权限检查错误
40
- */
41
- export declare function permissionDenied(customMessage?: string): CommandError;
42
- /**
43
- * 创建参数错误
44
- */
45
- export declare function invalidArgument(message: string): CommandError;
46
- /**
47
- * 创建未找到错误
48
- */
49
- export declare function notFound(resource: string): CommandError;
50
- /**
51
- * 创建已存在错误
52
- */
53
- export declare function alreadyExists(resource: string): CommandError;
@@ -1,120 +0,0 @@
1
- "use strict";
2
- /**
3
- * 命令错误处理中间件
4
- * 提供统一的错误处理和用户友好的错误消息
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.CommandError = exports.CommandErrorType = void 0;
8
- exports.executeCommand = executeCommand;
9
- exports.permissionDenied = permissionDenied;
10
- exports.invalidArgument = invalidArgument;
11
- exports.notFound = notFound;
12
- exports.alreadyExists = alreadyExists;
13
- const error_handler_1 = require("../utils/error-handler");
14
- const error_tracker_1 = require("../utils/error-tracker");
15
- const logger_1 = require("../utils/logger");
16
- /**
17
- * 命令错误类型
18
- */
19
- var CommandErrorType;
20
- (function (CommandErrorType) {
21
- CommandErrorType["PERMISSION_DENIED"] = "PERMISSION_DENIED";
22
- CommandErrorType["INVALID_ARGUMENT"] = "INVALID_ARGUMENT";
23
- CommandErrorType["NOT_FOUND"] = "NOT_FOUND";
24
- CommandErrorType["ALREADY_EXISTS"] = "ALREADY_EXISTS";
25
- CommandErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
26
- CommandErrorType["INTERNAL_ERROR"] = "INTERNAL_ERROR";
27
- })(CommandErrorType || (exports.CommandErrorType = CommandErrorType = {}));
28
- /**
29
- * 命令错误类
30
- */
31
- class CommandError extends Error {
32
- type;
33
- details;
34
- constructor(type, message, details) {
35
- super(message);
36
- this.type = type;
37
- this.details = details;
38
- this.name = 'CommandError';
39
- }
40
- }
41
- exports.CommandError = CommandError;
42
- /**
43
- * 包装命令执行,提供统一的错误处理
44
- */
45
- async function executeCommand(ctx, config, operationName, handler) {
46
- try {
47
- return await handler();
48
- }
49
- catch (error) {
50
- // 如果是 CommandError,使用自定义消息
51
- if (error instanceof CommandError) {
52
- logError(config, operationName, error);
53
- return formatCommandError(error);
54
- }
55
- // 其他错误使用友好错误消息
56
- logError(config, operationName, error);
57
- const friendlyMessage = (0, error_handler_1.getFriendlyErrorMessage)(error, operationName);
58
- return `${operationName}失败: ${friendlyMessage}`;
59
- }
60
- }
61
- /**
62
- * 记录命令错误
63
- */
64
- function logError(config, operation, error) {
65
- const normalizedError = (0, error_handler_1.normalizeError)(error, `${operation} failed`);
66
- const context = {
67
- command: operation,
68
- };
69
- if (error instanceof CommandError) {
70
- context.commandErrorType = error.type;
71
- }
72
- if (normalizedError && typeof normalizedError.code === 'string') {
73
- context.errorCode = normalizedError.code;
74
- }
75
- (0, logger_1.debugError)(config, normalizedError, `${operation} error`, context);
76
- (0, error_tracker_1.trackError)(normalizedError, context);
77
- }
78
- /**
79
- * 格式化命令错误消息
80
- */
81
- function formatCommandError(error) {
82
- switch (error.type) {
83
- case CommandErrorType.PERMISSION_DENIED:
84
- return error.message;
85
- case CommandErrorType.INVALID_ARGUMENT:
86
- return `参数错误: ${error.message}`;
87
- case CommandErrorType.NOT_FOUND:
88
- return `未找到: ${error.message}`;
89
- case CommandErrorType.ALREADY_EXISTS:
90
- return `已存在: ${error.message}`;
91
- case CommandErrorType.NETWORK_ERROR:
92
- return `网络错误: ${error.message}`;
93
- default:
94
- return error.message || '操作失败,请稍后重试';
95
- }
96
- }
97
- /**
98
- * 创建权限检查错误
99
- */
100
- function permissionDenied(customMessage) {
101
- return new CommandError(CommandErrorType.PERMISSION_DENIED, customMessage || '权限不足');
102
- }
103
- /**
104
- * 创建参数错误
105
- */
106
- function invalidArgument(message) {
107
- return new CommandError(CommandErrorType.INVALID_ARGUMENT, message);
108
- }
109
- /**
110
- * 创建未找到错误
111
- */
112
- function notFound(resource) {
113
- return new CommandError(CommandErrorType.NOT_FOUND, resource);
114
- }
115
- /**
116
- * 创建已存在错误
117
- */
118
- function alreadyExists(resource) {
119
- return new CommandError(CommandErrorType.ALREADY_EXISTS, resource);
120
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * RSS 订阅命令
3
- * 处理 RSS 订阅的添加、删除、列表、关注等操作
4
- */
5
- import { Context } from 'koishi';
6
- import { Config } from '../types';
7
- import { RssItemProcessor } from '../core/item-processor';
8
- /**
9
- * 注册 RSS 订阅命令
10
- */
11
- export declare function registerRssSubscribeCommand(ctx: Context, config: Config, processor: RssItemProcessor, getRssDataLocal: (url: string, arg: any) => Promise<any[]>, parseRssItem: (item: any, arg: any, authorId: string | number) => Promise<string>, parseQuickUrlLocal: (url: string) => string, parsePubDateLocal: (pubDate: any) => Date): void;