@anyul/koishi-plugin-rss 5.3.4 → 5.3.11

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.
@@ -100,7 +100,7 @@ const parseContent = (content, attr = undefined) => {
100
100
  else if (Object.prototype.toString.call(content) === '[object Object]') {
101
101
  return Object.values(content).reduce((t, v) => {
102
102
  if (v && (typeof v == 'string' || v?.join)) {
103
- let text = v?.join("") || v;
103
+ const text = v?.join("") || v;
104
104
  return (typeof text === 'string' && text.length > t.length) ? text : t;
105
105
  }
106
106
  else {
@@ -143,13 +143,13 @@ const parseTemplateContent = (template, item) => {
143
143
  exports.parseTemplateContent = parseTemplateContent;
144
144
  // 快速URL解析函数
145
145
  const parseQuickUrl = (url, rssHubUrl, quickList) => {
146
- let correntQuickObj = quickList.find(i => new RegExp(`^${i.prefix}:`).test(url));
146
+ const correntQuickObj = quickList.find(i => new RegExp(`^${i.prefix}:`).test(url));
147
147
  if (!correntQuickObj)
148
148
  return url;
149
- let routeMatch = url.match(new RegExp(`(?<=^${correntQuickObj.prefix}:).*`));
149
+ const routeMatch = url.match(new RegExp(`(?<=^${correntQuickObj.prefix}:).*`));
150
150
  if (!routeMatch)
151
151
  return url;
152
- let route = routeMatch[0];
152
+ const route = routeMatch[0];
153
153
  // 复用模块级 parseTemplateContent,避免内嵌重复实现
154
154
  const rUrl = (0, exports.parseTemplateContent)(correntQuickObj.replace, { rsshub: rssHubUrl, route });
155
155
  return rUrl;
@@ -163,7 +163,7 @@ const cleanContent = (htmlContent) => {
163
163
  $('style').remove();
164
164
  $('img').remove();
165
165
  $('video').remove();
166
- let plainText = $.text().replace(/\s+/g, ' ').trim();
166
+ const plainText = $.text().replace(/\s+/g, ' ').trim();
167
167
  return plainText;
168
168
  };
169
169
  exports.cleanContent = cleanContent;
@@ -99,7 +99,7 @@ const createHttpFunction = (ctx, config, requestManager) => {
99
99
  const makeRequest = async () => {
100
100
  // 基础配置
101
101
  requestDebug(`[DEBUG_PROXY] fetcher makeRequest arg.proxyAgent: ${JSON.stringify(arg?.proxyAgent)}`, 'request', 'details');
102
- let configObj = {
102
+ const configObj = {
103
103
  timeout: requestTimeout,
104
104
  headers: {
105
105
  'User-Agent': config.net.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
@@ -9,8 +9,12 @@ exports.getNextUpdateTime = getNextUpdateTime;
9
9
  exports.setNextUpdateTime = setNextUpdateTime;
10
10
  function normalizeBasicConfig(basic) {
11
11
  const normalized = { ...(basic || {}) };
12
- const mergeVideo = normalized.mergeVideo ?? normalized.margeVideo;
13
- const resendUpdatedContent = normalized.resendUpdatedContent ?? normalized.resendUpdataContent;
12
+ // 读取归一:typo(旧拼写)优先。理由——schema 会为声明的规范名注入默认值
13
+ // (如 mergeVideo:false),若用 `correct ?? typo` 会被默认值短路,吞掉老用户
14
+ // 真实意图(margeVideo:true)。typo 存在则必来自历史用户数据,应优先采纳。
15
+ // 写入仍双写两个键(保持运行时兼容,下游直接读 margeVideo 仍可拿到值)。
16
+ const mergeVideo = normalized.margeVideo ?? normalized.mergeVideo;
17
+ const resendUpdatedContent = normalized.resendUpdataContent ?? normalized.resendUpdatedContent;
14
18
  if (mergeVideo !== undefined) {
15
19
  normalized.mergeVideo = mergeVideo;
16
20
  normalized.margeVideo = mergeVideo;
@@ -47,10 +51,12 @@ function getNextUpdateTime(arg) {
47
51
  }
48
52
  function setNextUpdateTime(target, nextUpdateTime) {
49
53
  if (nextUpdateTime === undefined) {
54
+ // 清理时双删:删规范名的同时顺手清掉残留的旧拼写名(老数据可能仍有)
50
55
  delete target.nextUpdateTime;
51
56
  delete target.nextUpdataTime;
52
57
  return;
53
58
  }
59
+ // 只写规范名:不再回写 nextUpdataTime,让旧拼写从数据中自然淘汰。
60
+ // 读取侧 getNextUpdateTime 已用 `nextUpdateTime ?? nextUpdataTime` 兜底老数据。
54
61
  target.nextUpdateTime = nextUpdateTime;
55
- target.nextUpdataTime = nextUpdateTime;
56
62
  }
@@ -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
+ }
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.4",
4
+ "version": "5.3.11",
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
  }