@anyul/koishi-plugin-rss 4.8.13 → 4.8.14
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/index.d.ts +18 -11
- package/lib/index.js +181 -49
- package/package.json +1 -1
- package/readme.md +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
|
+
declare module 'koishi' {
|
|
3
|
+
interface Context {
|
|
4
|
+
assets?: {
|
|
5
|
+
upload(dataUrl: string, filename: string): Promise<string>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
}
|
|
2
9
|
export declare const name = "@anyul/koishi-plugin-rss";
|
|
3
10
|
export declare const inject: {
|
|
4
11
|
required: string[];
|
|
@@ -35,8 +42,8 @@ interface BasicConfig {
|
|
|
35
42
|
firstLoad?: boolean;
|
|
36
43
|
urlDeduplication?: boolean;
|
|
37
44
|
resendUpdataContent: 'disable' | 'latest' | 'all';
|
|
38
|
-
imageMode?: 'base64' | 'File';
|
|
39
|
-
videoMode?: 'filter' | 'href' | 'base64' | 'File';
|
|
45
|
+
imageMode?: 'base64' | 'File' | 'assets';
|
|
46
|
+
videoMode?: 'filter' | 'href' | 'base64' | 'File' | 'assets';
|
|
40
47
|
autoSplitImage?: boolean;
|
|
41
48
|
cacheDir?: string;
|
|
42
49
|
replaceDir?: string;
|
|
@@ -102,7 +109,7 @@ export interface rssArg {
|
|
|
102
109
|
split?: number;
|
|
103
110
|
nextUpdataTime?: number;
|
|
104
111
|
}
|
|
105
|
-
export declare const usage = "\nRSS-OWL \u8BA2\u9605\u5668\u4F7F\u7528\u8BF4\u660E\n\n\u57FA\u672C\u547D\u4EE4:\n rsso
|
|
112
|
+
export declare const usage = "\nRSS-OWL \u8BA2\u9605\u5668\u4F7F\u7528\u8BF4\u660E\n\n\u57FA\u672C\u547D\u4EE4:\n rsso <url> - \u8BA2\u9605RSS\u94FE\u63A5\n rsso -l - \u67E5\u770B\u8BA2\u9605\u5217\u8868\n rsso -l [id] - \u67E5\u770B\u8BA2\u9605\u8BE6\u60C5\n rsso -r <content> - \u5220\u9664\u8BA2\u9605(\u9700\u8981\u6743\u9650)\n rsso -T <url> - \u6D4B\u8BD5\u8BA2\u9605\n\n\u5E38\u7528\u9009\u9879:\n -i <template> - \u8BBE\u7F6E\u6D88\u606F\u6A21\u677F\n \u53EF\u9009\u503C: content(\u6587\u5B57) | default(\u56FE\u7247) | custom(\u81EA\u5B9A\u4E49) | only text | only media \u7B49\n -t <title> - \u81EA\u5B9A\u4E49\u8BA2\u9605\u6807\u9898\n -a <arg> - \u81EA\u5B9A\u4E49\u914D\u7F6E (\u683C\u5F0F: key:value,key2:value2)\n \u4F8B\u5982: -a timeout:30,merge:true\n\n\u9AD8\u7EA7\u9009\u9879:\n -f <content> - \u5173\u6CE8\u8BA2\u9605\uFF0C\u66F4\u65B0\u65F6\u63D0\u9192\n -fAll <content> - \u5168\u4F53\u5173\u6CE8(\u9700\u8981\u9AD8\u7EA7\u6743\u9650)\n -target <groupId> - \u8DE8\u7FA4\u8BA2\u9605(\u9700\u8981\u9AD8\u7EA7\u6743\u9650)\n -d <time> - \u5B9A\u65F6\u63A8\u9001 (\u683C\u5F0F: \"HH:mm/\u6570\u91CF\" \u6216 \"HH:mm\")\n \u4F8B\u5982: -d \"08:00/5\" \u8868\u793A\u6BCF\u59298\u70B9\u63A8\u90015\u6761\n -p <id> - \u624B\u52A8\u62C9\u53D6\u6700\u65B0\u5185\u5BB9\n\n\u5FEB\u901F\u8BA2\u9605:\n rsso -q - \u67E5\u770B\u5FEB\u901F\u8BA2\u9605\u5217\u8868\n rsso -q [\u7F16\u53F7] - \u67E5\u770B\u5FEB\u901F\u8BA2\u9605\u8BE6\u60C5\n rsso -T tg:channel_name - \u5FEB\u901F\u8BA2\u9605Telegram\u9891\u9053\n\nAssets \u56FE\u7247/\u89C6\u9891\u670D\u52A1\u914D\u7F6E (\u63A8\u8350):\n \u4F7F\u7528 assets \u670D\u52A1\u53EF\u4EE5\u907F\u514D Base64 \u8D85\u957F\u95EE\u9898\n 1. \u5728\u63D2\u4EF6\u5E02\u573A\u5B89\u88C5 assets-xxx \u63D2\u4EF6 (\u5982 assets-local, assets-s3, assets-smms \u7B49)\n 2. \u5728\u5BF9\u5E94\u63D2\u4EF6\u4E2D\u914D\u7F6E\u5B58\u50A8\u4FE1\u606F (AccessKey, Secret, Bucket \u7B49)\n 3. \u5728 RSS-Owl \u57FA\u7840\u8BBE\u7F6E\u4E2D\u5C06 imageMode/videoMode \u8BBE\u7F6E\u4E3A 'assets'\n 4. \u63D2\u4EF6\u4F1A\u81EA\u52A8\u4E0A\u4F20\u56FE\u7247/\u89C6\u9891\u5230\u4F60\u7684\u56FE\u5E8A\u670D\u52A1\n\n\u914D\u7F6E\u793A\u4F8B:\n rsso -T -i content \"https://example.com/rss\"\n rsso \"https://example.com/rss\" -t \"\u6211\u7684\u8BA2\u9605\" -a \"timeout:60,merge:true\"\n rsso -d \"09:00/3\" \"https://example.com/rss\"\n\n";
|
|
106
113
|
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
107
114
|
basic: Schema<Schemastery.ObjectS<{
|
|
108
115
|
defaultTemplate: Schema<string, string>;
|
|
@@ -115,8 +122,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
115
122
|
firstLoad: Schema<boolean, boolean>;
|
|
116
123
|
urlDeduplication: Schema<boolean, boolean>;
|
|
117
124
|
resendUpdataContent: Schema<"disable" | "latest" | "all", "disable" | "latest" | "all">;
|
|
118
|
-
imageMode: Schema<"base64" | "File", "base64" | "File">;
|
|
119
|
-
videoMode: Schema<"base64" | "File" | "filter" | "href", "base64" | "File" | "filter" | "href">;
|
|
125
|
+
imageMode: Schema<"assets" | "base64" | "File", "assets" | "base64" | "File">;
|
|
126
|
+
videoMode: Schema<"assets" | "base64" | "File" | "filter" | "href", "assets" | "base64" | "File" | "filter" | "href">;
|
|
120
127
|
margeVideo: Schema<boolean, boolean>;
|
|
121
128
|
usePoster: Schema<boolean, boolean>;
|
|
122
129
|
autoSplitImage: Schema<boolean, boolean>;
|
|
@@ -133,8 +140,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
133
140
|
firstLoad: Schema<boolean, boolean>;
|
|
134
141
|
urlDeduplication: Schema<boolean, boolean>;
|
|
135
142
|
resendUpdataContent: Schema<"disable" | "latest" | "all", "disable" | "latest" | "all">;
|
|
136
|
-
imageMode: Schema<"base64" | "File", "base64" | "File">;
|
|
137
|
-
videoMode: Schema<"base64" | "File" | "filter" | "href", "base64" | "File" | "filter" | "href">;
|
|
143
|
+
imageMode: Schema<"assets" | "base64" | "File", "assets" | "base64" | "File">;
|
|
144
|
+
videoMode: Schema<"assets" | "base64" | "File" | "filter" | "href", "assets" | "base64" | "File" | "filter" | "href">;
|
|
138
145
|
margeVideo: Schema<boolean, boolean>;
|
|
139
146
|
usePoster: Schema<boolean, boolean>;
|
|
140
147
|
autoSplitImage: Schema<boolean, boolean>;
|
|
@@ -189,8 +196,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
189
196
|
firstLoad: Schema<boolean, boolean>;
|
|
190
197
|
urlDeduplication: Schema<boolean, boolean>;
|
|
191
198
|
resendUpdataContent: Schema<"disable" | "latest" | "all", "disable" | "latest" | "all">;
|
|
192
|
-
imageMode: Schema<"base64" | "File", "base64" | "File">;
|
|
193
|
-
videoMode: Schema<"base64" | "File" | "filter" | "href", "base64" | "File" | "filter" | "href">;
|
|
199
|
+
imageMode: Schema<"assets" | "base64" | "File", "assets" | "base64" | "File">;
|
|
200
|
+
videoMode: Schema<"assets" | "base64" | "File" | "filter" | "href", "assets" | "base64" | "File" | "filter" | "href">;
|
|
194
201
|
margeVideo: Schema<boolean, boolean>;
|
|
195
202
|
usePoster: Schema<boolean, boolean>;
|
|
196
203
|
autoSplitImage: Schema<boolean, boolean>;
|
|
@@ -207,8 +214,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
207
214
|
firstLoad: Schema<boolean, boolean>;
|
|
208
215
|
urlDeduplication: Schema<boolean, boolean>;
|
|
209
216
|
resendUpdataContent: Schema<"disable" | "latest" | "all", "disable" | "latest" | "all">;
|
|
210
|
-
imageMode: Schema<"base64" | "File", "base64" | "File">;
|
|
211
|
-
videoMode: Schema<"base64" | "File" | "filter" | "href", "base64" | "File" | "filter" | "href">;
|
|
217
|
+
imageMode: Schema<"assets" | "base64" | "File", "assets" | "base64" | "File">;
|
|
218
|
+
videoMode: Schema<"assets" | "base64" | "File" | "filter" | "href", "assets" | "base64" | "File" | "filter" | "href">;
|
|
212
219
|
margeVideo: Schema<boolean, boolean>;
|
|
213
220
|
usePoster: Schema<boolean, boolean>;
|
|
214
221
|
autoSplitImage: Schema<boolean, boolean>;
|
package/lib/index.js
CHANGED
|
@@ -42,7 +42,6 @@ const koishi_1 = require("koishi");
|
|
|
42
42
|
const axios_1 = __importDefault(require("axios"));
|
|
43
43
|
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
44
44
|
const cheerio = __importStar(require("cheerio"));
|
|
45
|
-
// import { } from '@koishijs/assets'
|
|
46
45
|
const X2JS = require("x2js");
|
|
47
46
|
const x2js = new X2JS();
|
|
48
47
|
const logger = new koishi_1.Logger('rss-owl');
|
|
@@ -50,44 +49,50 @@ exports.name = '@anyul/koishi-plugin-rss';
|
|
|
50
49
|
const url_1 = require("url");
|
|
51
50
|
const fs = __importStar(require("fs"));
|
|
52
51
|
const path = __importStar(require("path"));
|
|
53
|
-
exports.inject = { required: ["database"], optional: ["puppeteer", "censor"] };
|
|
52
|
+
exports.inject = { required: ["database"], optional: ["puppeteer", "censor", "assets"] };
|
|
54
53
|
const debugLevel = ["disable", "error", "info", "details"];
|
|
55
54
|
exports.usage = `
|
|
56
55
|
RSS-OWL 订阅器使用说明
|
|
57
56
|
|
|
58
57
|
基本命令:
|
|
59
|
-
rsso
|
|
58
|
+
rsso <url> - 订阅RSS链接
|
|
60
59
|
rsso -l - 查看订阅列表
|
|
61
60
|
rsso -l [id] - 查看订阅详情
|
|
62
|
-
rsso -r
|
|
63
|
-
rsso -T
|
|
61
|
+
rsso -r <content> - 删除订阅(需要权限)
|
|
62
|
+
rsso -T <url> - 测试订阅
|
|
64
63
|
|
|
65
64
|
常用选项:
|
|
66
|
-
-i
|
|
65
|
+
-i <template> - 设置消息模板
|
|
67
66
|
可选值: content(文字) | default(图片) | custom(自定义) | only text | only media 等
|
|
68
|
-
-t
|
|
69
|
-
-a
|
|
67
|
+
-t <title> - 自定义订阅标题
|
|
68
|
+
-a <arg> - 自定义配置 (格式: key:value,key2:value2)
|
|
70
69
|
例如: -a timeout:30,merge:true
|
|
71
70
|
|
|
72
71
|
高级选项:
|
|
73
|
-
-f
|
|
74
|
-
-fAll
|
|
75
|
-
-target
|
|
76
|
-
-d
|
|
72
|
+
-f <content> - 关注订阅,更新时提醒
|
|
73
|
+
-fAll <content> - 全体关注(需要高级权限)
|
|
74
|
+
-target <groupId> - 跨群订阅(需要高级权限)
|
|
75
|
+
-d <time> - 定时推送 (格式: "HH:mm/数量" 或 "HH:mm")
|
|
77
76
|
例如: -d "08:00/5" 表示每天8点推送5条
|
|
78
|
-
-p
|
|
77
|
+
-p <id> - 手动拉取最新内容
|
|
79
78
|
|
|
80
79
|
快速订阅:
|
|
81
80
|
rsso -q - 查看快速订阅列表
|
|
82
81
|
rsso -q [编号] - 查看快速订阅详情
|
|
83
82
|
rsso -T tg:channel_name - 快速订阅Telegram频道
|
|
84
83
|
|
|
84
|
+
Assets 图片/视频服务配置 (推荐):
|
|
85
|
+
使用 assets 服务可以避免 Base64 超长问题
|
|
86
|
+
1. 在插件市场安装 assets-xxx 插件 (如 assets-local, assets-s3, assets-smms 等)
|
|
87
|
+
2. 在对应插件中配置存储信息 (AccessKey, Secret, Bucket 等)
|
|
88
|
+
3. 在 RSS-Owl 基础设置中将 imageMode/videoMode 设置为 'assets'
|
|
89
|
+
4. 插件会自动上传图片/视频到你的图床服务
|
|
90
|
+
|
|
85
91
|
配置示例:
|
|
86
92
|
rsso -T -i content "https://example.com/rss"
|
|
87
93
|
rsso "https://example.com/rss" -t "我的订阅" -a "timeout:60,merge:true"
|
|
88
94
|
rsso -d "09:00/3" "https://example.com/rss"
|
|
89
95
|
|
|
90
|
-
更多信息请访问: https://github.com/borraken/koishi-plugin-rss-owl
|
|
91
96
|
`;
|
|
92
97
|
const templateList = ['auto', 'content', 'only text', 'only media', 'only image', 'only video', 'proto', 'default', 'only description', 'custom', 'link'];
|
|
93
98
|
exports.Config = koishi_1.Schema.object({
|
|
@@ -103,8 +108,8 @@ exports.Config = koishi_1.Schema.object({
|
|
|
103
108
|
firstLoad: koishi_1.Schema.boolean().description('首次订阅时是否发送最后的更新').default(true),
|
|
104
109
|
urlDeduplication: koishi_1.Schema.boolean().description('同群组中不允许重复添加相同订阅').default(true),
|
|
105
110
|
resendUpdataContent: koishi_1.Schema.union(['disable', 'latest', 'all']).description('当内容更新时再次发送').default('disable').experimental(),
|
|
106
|
-
imageMode: koishi_1.Schema.union(['base64', 'File']).description('
|
|
107
|
-
videoMode: koishi_1.Schema.union(['filter', 'href', 'base64', 'File']).description('视频发送模式(iframe标签内的视频无法处理)<br
|
|
111
|
+
imageMode: koishi_1.Schema.union(['base64', 'File', 'assets']).description('图片发送模式<br>\`base64\` Base64格式(兼容性好但容易超长)<br>\`File\` 本地文件(不支持沙盒环境)<br>\`assets\` Assets服务(推荐,需安装assets-xxx插件并配置)').default('base64'),
|
|
112
|
+
videoMode: koishi_1.Schema.union(['filter', 'href', 'base64', 'File', 'assets']).description('视频发送模式(iframe标签内的视频无法处理)<br>\`filter\` 过滤视频,含有视频的推送将不会被发送<br>\`href\` 使用视频网络地址直接发送<br>\`base64\` 下载后以base64格式发送<br>\`File\` 下载后以文件发送<br>\`assets\` 上传到assets服务(需安装assets-xxx插件并配置)').default('href'),
|
|
108
113
|
margeVideo: koishi_1.Schema.boolean().default(false).description('以合并消息发送视频'),
|
|
109
114
|
usePoster: koishi_1.Schema.boolean().default(false).description('加载视频封面'),
|
|
110
115
|
autoSplitImage: koishi_1.Schema.boolean().description('垂直拆分大尺寸图片,解决部分适配器发不出长图的问题').default(true),
|
|
@@ -222,6 +227,25 @@ function apply(ctx, config) {
|
|
|
222
227
|
debug('imgUrl:' + url, '', 'details');
|
|
223
228
|
if (!url)
|
|
224
229
|
return '';
|
|
230
|
+
// 如果配置了 assets 且 ctx 中有该服务,优先处理
|
|
231
|
+
if (config.basic.imageMode === 'assets' && ctx.assets && !useBase64Mode) {
|
|
232
|
+
try {
|
|
233
|
+
let res = await $http(url, arg, { responseType: 'arraybuffer', timeout: 30000 });
|
|
234
|
+
let contentType = res.headers["content-type"] || 'image/jpeg'; // 兜底 contentType
|
|
235
|
+
let suffix = contentType?.split('/')[1] || 'jpg';
|
|
236
|
+
// ★★★ 修复点:转为 Data URL 字符串再上传 ★★★
|
|
237
|
+
const buffer = Buffer.from(res.data, 'binary');
|
|
238
|
+
const base64 = `data:${contentType};base64,${buffer.toString('base64')}`;
|
|
239
|
+
let assetUrl = await ctx.assets.upload(base64, `rss-img-${Date.now()}.${suffix}`);
|
|
240
|
+
debug(`Assets 上传成功: ${assetUrl}`, 'assets', 'info');
|
|
241
|
+
return assetUrl;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
debug(`Assets 上传失败,降级为 Base64: ${error}`, 'assets error', 'error');
|
|
245
|
+
// 降级到 base64 模式(不设置 useBase64Mode 避免递归)
|
|
246
|
+
// 这里直接使用 base64 逻辑
|
|
247
|
+
}
|
|
248
|
+
}
|
|
225
249
|
let res;
|
|
226
250
|
try {
|
|
227
251
|
res = await $http(url, arg, { responseType: 'arraybuffer', timeout: 30000 });
|
|
@@ -235,7 +259,7 @@ function apply(ctx, config) {
|
|
|
235
259
|
let prefix = res.headers["content-type"] || ('image/' + (prefixList.find(i => new RegExp(i).test(url)) || 'jpeg'));
|
|
236
260
|
let base64Prefix = `data:${prefix};base64,`;
|
|
237
261
|
let base64Data = base64Prefix + Buffer.from(res.data, 'binary').toString('base64');
|
|
238
|
-
if (config.basic.imageMode == 'base64' || useBase64Mode) {
|
|
262
|
+
if (config.basic.imageMode == 'base64' || useBase64Mode || config.basic.imageMode === 'assets') {
|
|
239
263
|
return base64Data;
|
|
240
264
|
}
|
|
241
265
|
else if (config.basic.imageMode == 'File') {
|
|
@@ -249,29 +273,62 @@ function apply(ctx, config) {
|
|
|
249
273
|
if (config.basic.videoMode == "href") {
|
|
250
274
|
return src;
|
|
251
275
|
}
|
|
252
|
-
|
|
276
|
+
// assets 模式
|
|
277
|
+
if (config.basic.videoMode === 'assets' && ctx.assets) {
|
|
253
278
|
try {
|
|
254
279
|
res = await $http(src, arg, { responseType: 'arraybuffer', timeout: 0 });
|
|
280
|
+
let contentType = res.headers["content-type"] || 'video/mp4';
|
|
281
|
+
let suffix = contentType?.split('/')[1] || 'mp4';
|
|
282
|
+
// ★★★ 修复点:转为 Data URL 字符串再上传 ★★★
|
|
283
|
+
const buffer = Buffer.from(res.data, 'binary');
|
|
284
|
+
// 注意:视频 Base64 可能会非常长,部分 assets 插件可能处理较慢,但比 Buffer 兼容性好
|
|
285
|
+
const base64 = `data:${contentType};base64,${buffer.toString('base64')}`;
|
|
286
|
+
let assetUrl = await ctx.assets.upload(base64, `rss-video-${Date.now()}.${suffix}`);
|
|
287
|
+
debug(`视频 Assets 上传成功: ${assetUrl}`, 'assets', 'info');
|
|
288
|
+
return assetUrl;
|
|
255
289
|
}
|
|
256
290
|
catch (error) {
|
|
257
|
-
debug(
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
let prefix = res.headers["content-type"];
|
|
261
|
-
let base64Prefix = `data:${prefix};base64,`;
|
|
262
|
-
let base64Data = base64Prefix + Buffer.from(res.data, 'binary').toString('base64');
|
|
263
|
-
if (config.basic.videoMode == 'base64') {
|
|
264
|
-
return base64Data;
|
|
265
|
-
}
|
|
266
|
-
else if (config.basic.videoMode == 'File') {
|
|
267
|
-
let fileUrl = await writeCacheFile(base64Data);
|
|
268
|
-
return fileUrl;
|
|
291
|
+
debug(`视频 Assets 上传失败,降级为 Base64: ${error}`, 'assets error', 'error');
|
|
292
|
+
// 降级到 base64 模式
|
|
269
293
|
}
|
|
270
294
|
}
|
|
295
|
+
try {
|
|
296
|
+
res = await $http(src, arg, { responseType: 'arraybuffer', timeout: 0 });
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
debug(`视频请求失败: ${error}`, 'video error', 'error');
|
|
300
|
+
return '';
|
|
301
|
+
}
|
|
302
|
+
let prefix = res.headers["content-type"];
|
|
303
|
+
let base64Prefix = `data:${prefix};base64,`;
|
|
304
|
+
let base64Data = base64Prefix + Buffer.from(res.data, 'binary').toString('base64');
|
|
305
|
+
if (config.basic.videoMode == 'base64' || config.basic.videoMode === 'assets') {
|
|
306
|
+
return base64Data;
|
|
307
|
+
}
|
|
308
|
+
else if (config.basic.videoMode == 'File') {
|
|
309
|
+
let fileUrl = await writeCacheFile(base64Data);
|
|
310
|
+
return fileUrl;
|
|
311
|
+
}
|
|
271
312
|
};
|
|
272
313
|
const puppeteerToFile = async (puppeteer) => {
|
|
273
|
-
let base64 = /(?<=src=").+?(?=")/.exec(puppeteer)[0];
|
|
314
|
+
let base64 = /(?<=src=").+?(?=")/.exec(puppeteer)?.[0];
|
|
315
|
+
if (!base64)
|
|
316
|
+
return puppeteer;
|
|
274
317
|
const buffer = Buffer.from(base64.substring(base64.indexOf(',') + 1), 'base64');
|
|
318
|
+
// assets 模式
|
|
319
|
+
if (config.basic.imageMode === 'assets' && ctx.assets) {
|
|
320
|
+
try {
|
|
321
|
+
// ★★★ 修复点:直接传递 base64 字符串给 upload,不要转 Buffer ★★★
|
|
322
|
+
// base64 变量本身就是 "data:image/png;base64,..." 格式
|
|
323
|
+
const url = await ctx.assets.upload(base64, `rss-screenshot-${Date.now()}.png`);
|
|
324
|
+
debug(`截图 Assets 上传成功: ${url}`, 'assets', 'info');
|
|
325
|
+
return `<img src="${url}"/>`;
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
debug(`截图 Assets 上传失败,降级为 File: ${error}`, 'assets error', 'error');
|
|
329
|
+
// 降级到 File 模式
|
|
330
|
+
}
|
|
331
|
+
}
|
|
275
332
|
const MB = buffer.length / 1e+6;
|
|
276
333
|
debug("MB: " + MB, 'file size', 'details');
|
|
277
334
|
return `<file src="${await writeCacheFile(base64)}"/>`;
|
|
@@ -452,6 +509,20 @@ function apply(ctx, config) {
|
|
|
452
509
|
await page.setContent(htmlContent);
|
|
453
510
|
if (!config.basic.autoSplitImage) {
|
|
454
511
|
const image = await page.screenshot({ type: "png" });
|
|
512
|
+
// assets 模式
|
|
513
|
+
if (config.basic.imageMode === 'assets' && ctx.assets) {
|
|
514
|
+
try {
|
|
515
|
+
// ★★★ 修复点:Buffer 转 Data URL ★★★
|
|
516
|
+
const base64 = `data:image/png;base64,${image.toString('base64')}`;
|
|
517
|
+
const url = await ctx.assets.upload(base64, `rss-shot-${Date.now()}.png`);
|
|
518
|
+
debug(`HTML截图 Assets 上传成功: ${url}`, 'assets', 'info');
|
|
519
|
+
return koishi_1.h.image(url);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
debug(`HTML截图 Assets 上传失败,降级为 Base64: ${error}`, 'assets error', 'error');
|
|
523
|
+
// 降级到 base64
|
|
524
|
+
}
|
|
525
|
+
}
|
|
455
526
|
return koishi_1.h.image(image, 'image/png');
|
|
456
527
|
}
|
|
457
528
|
let [height, width, x, y] = await page.evaluate(() => [
|
|
@@ -465,6 +536,20 @@ function apply(ctx, config) {
|
|
|
465
536
|
const split = Math.ceil(height / size);
|
|
466
537
|
if (!split) {
|
|
467
538
|
const image = await page.screenshot({ type: "png", clip: { x, y, width, height } });
|
|
539
|
+
// assets 模式
|
|
540
|
+
if (config.basic.imageMode === 'assets' && ctx.assets) {
|
|
541
|
+
try {
|
|
542
|
+
// ★★★ 修复点:Buffer 转 Data URL ★★★
|
|
543
|
+
const base64 = `data:image/png;base64,${image.toString('base64')}`;
|
|
544
|
+
const url = await ctx.assets.upload(base64, `rss-shot-${Date.now()}.png`);
|
|
545
|
+
debug(`HTML截图 Assets 上传成功: ${url}`, 'assets', 'info');
|
|
546
|
+
return koishi_1.h.image(url);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
debug(`HTML截图 Assets 上传失败,降级为 Base64: ${error}`, 'assets error', 'error');
|
|
550
|
+
// 降级到 base64
|
|
551
|
+
}
|
|
552
|
+
}
|
|
468
553
|
return koishi_1.h.image(image, 'image/png');
|
|
469
554
|
}
|
|
470
555
|
debug({ height, width, split }, 'split img', 'details');
|
|
@@ -479,6 +564,22 @@ function apply(ctx, config) {
|
|
|
479
564
|
height: reduceHeight(i)
|
|
480
565
|
}
|
|
481
566
|
})));
|
|
567
|
+
// assets 模式
|
|
568
|
+
if (config.basic.imageMode === 'assets' && ctx.assets) {
|
|
569
|
+
try {
|
|
570
|
+
// ★★★ 修复点:Buffer 数组转 Data URL 数组 ★★★
|
|
571
|
+
const urls = await Promise.all(imgData.map((buf, i) => {
|
|
572
|
+
const base64 = `data:image/png;base64,${buf.toString('base64')}`;
|
|
573
|
+
return ctx.assets.upload(base64, `rss-split-${Date.now()}-${i}.png`);
|
|
574
|
+
}));
|
|
575
|
+
debug(`切割截图 Assets 上传成功: ${urls.length} 个文件`, 'assets', 'info');
|
|
576
|
+
return urls.map(u => koishi_1.h.image(u)).join("");
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
debug(`切割截图 Assets 上传失败,降级为 Base64: ${error}`, 'assets error', 'error');
|
|
580
|
+
// 降级到 base64
|
|
581
|
+
}
|
|
582
|
+
}
|
|
482
583
|
return imgData.map(i => koishi_1.h.image(i, 'image/png')).join("");
|
|
483
584
|
}
|
|
484
585
|
finally {
|
|
@@ -610,25 +711,31 @@ function apply(ctx, config) {
|
|
|
610
711
|
}
|
|
611
712
|
msg = parseContent(config.template.customRemark, { ...item, arg, description: msg });
|
|
612
713
|
await Promise.all(html('video').map(async (v, i) => videoList.push([await getVideoUrl(i.attribs.src, arg, true, i), (i.attribs.poster && config.basic.usePoster) ? await getImageUrl(i.attribs.poster, arg, true) : ""])));
|
|
613
|
-
|
|
714
|
+
// ★★★ 修复点:过滤掉没有 src 的视频 ★★★
|
|
715
|
+
msg += videoList.filter(([src]) => src).map(([src, poster]) => (0, koishi_1.h)('video', { src, poster })).join("");
|
|
614
716
|
}
|
|
615
717
|
else if (template == "content") {
|
|
616
718
|
html = cheerio.load(item.description);
|
|
617
719
|
let imgList = [];
|
|
618
720
|
html('img').map((key, i) => imgList.push(i.attribs.src));
|
|
619
721
|
imgList = [...new Set(imgList)];
|
|
722
|
+
// 获取所有图片链接
|
|
620
723
|
let imgBufferList = Object.assign({}, ...(await Promise.all(imgList.map(async (src) => ({ [src]: await getImageUrl(src, arg) })))));
|
|
724
|
+
// 占位符替换
|
|
621
725
|
html('img').replaceWith((key, Dom) => `<p>$img{{${imgList[key]}}}</p>`);
|
|
622
726
|
msg = html.text();
|
|
727
|
+
// ★★★ 修复点:如果 finalUrl 为空,返回空字符串,不要生成 <img src=""/> ★★★
|
|
623
728
|
item.description = msg.replace(/\$img\{\{(.*?)\}\}/g, (match) => {
|
|
624
729
|
let src = match.match(/\$img\{\{(.*?)\}\}/)[1];
|
|
625
|
-
|
|
730
|
+
let finalUrl = imgBufferList[src];
|
|
731
|
+
return finalUrl ? `<img src="${finalUrl}"/>` : '';
|
|
626
732
|
});
|
|
627
733
|
msg = parseContent(config.template.content, { ...item, arg });
|
|
628
|
-
// 【修复 1】: 删除了这里的 logger.info(msg),因为函数末尾还会打印一次
|
|
629
734
|
await Promise.all(html('video').map(async (v, i) => videoList.push([await getVideoUrl(i.attribs.src, arg, true, i), (i.attribs.poster && config.basic.usePoster) ? await getImageUrl(i.attribs.poster, arg, true) : ""])));
|
|
630
|
-
|
|
631
|
-
msg += videoList.map(([src, poster]) => (0, koishi_1.h)('
|
|
735
|
+
// ★★★ 修复点:过滤掉没有 src 的视频 ★★★
|
|
736
|
+
msg += videoList.filter(([src]) => src).map(([src, poster]) => (0, koishi_1.h)('video', { src, poster })).join("");
|
|
737
|
+
// ★★★ 修复点:过滤掉没有 src 的视频封面图 ★★★
|
|
738
|
+
msg += videoList.filter(([src, poster]) => poster).map(([src, poster]) => (0, koishi_1.h)('img', { src: poster })).join("");
|
|
632
739
|
}
|
|
633
740
|
else if (template == "only text") {
|
|
634
741
|
html = cheerio.load(item.description);
|
|
@@ -639,21 +746,25 @@ function apply(ctx, config) {
|
|
|
639
746
|
let imgList = [];
|
|
640
747
|
html('img').map((key, i) => imgList.push(i.attribs.src));
|
|
641
748
|
imgList = await Promise.all([...new Set(imgList)].map(async (src) => await getImageUrl(src, arg)));
|
|
642
|
-
|
|
749
|
+
// ★★★ 修复点:过滤空图片 ★★★
|
|
750
|
+
msg = imgList.filter(Boolean).map(img => `<img src="${img}"/>`).join("");
|
|
643
751
|
await Promise.all(html('video').map(async (v, i) => videoList.push([await getVideoUrl(i.attribs.src, arg, true, i), (i.attribs.poster && config.basic.usePoster) ? await getImageUrl(i.attribs.poster, arg, true) : ""])));
|
|
644
|
-
|
|
752
|
+
// ★★★ 修复点:过滤空视频 ★★★
|
|
753
|
+
msg += videoList.filter(([src]) => src).map(([src, poster]) => (0, koishi_1.h)('video', { src, poster })).join("");
|
|
645
754
|
}
|
|
646
755
|
else if (template == "only image") {
|
|
647
756
|
html = cheerio.load(item.description);
|
|
648
757
|
let imgList = [];
|
|
649
758
|
html('img').map((key, i) => imgList.push(i.attribs.src));
|
|
650
759
|
imgList = await Promise.all([...new Set(imgList)].map(async (src) => await getImageUrl(src, arg)));
|
|
651
|
-
|
|
760
|
+
// ★★★ 修复点:过滤空图片 ★★★
|
|
761
|
+
msg = imgList.filter(Boolean).map(img => `<img src="${img}"/>`).join("");
|
|
652
762
|
}
|
|
653
763
|
else if (template == "only video") {
|
|
654
764
|
html = cheerio.load(item.description);
|
|
655
765
|
await Promise.all(html('video').map(async (v, i) => videoList.push([await getVideoUrl(i.attribs.src, arg, true, i), (i.attribs.poster && config.basic.usePoster) ? await getImageUrl(i.attribs.poster, arg, true) : ""])));
|
|
656
|
-
|
|
766
|
+
// ★★★ 修复点:过滤掉没有 src 的视频 ★★★
|
|
767
|
+
msg += videoList.filter(([src]) => src).map(([src, poster]) => (0, koishi_1.h)('video', { src, poster })).join("");
|
|
657
768
|
}
|
|
658
769
|
else if (template == "proto") {
|
|
659
770
|
msg = item.description;
|
|
@@ -676,7 +787,8 @@ function apply(ctx, config) {
|
|
|
676
787
|
if (config.basic.imageMode == 'File')
|
|
677
788
|
msg = await puppeteerToFile(msg);
|
|
678
789
|
await Promise.all(html('video').map(async (v, i) => videoList.push([await getVideoUrl(i.attribs.src, arg, true, i), (i.attribs.poster && config.basic.usePoster) ? await getImageUrl(i.attribs.poster, arg, true) : ""])));
|
|
679
|
-
|
|
790
|
+
// ★★★ 修复点:过滤掉没有 src 的视频 ★★★
|
|
791
|
+
msg += videoList.filter(([src]) => src).map(([src, poster]) => (0, koishi_1.h)('video', { src, poster })).join("");
|
|
680
792
|
}
|
|
681
793
|
else if (template == "only description") {
|
|
682
794
|
item.description = parseContent(getDescriptionTemplate(config.template.bodyWidth, config.template.bodyPadding, config.template.bodyFontSize), { ...item, arg });
|
|
@@ -693,7 +805,8 @@ function apply(ctx, config) {
|
|
|
693
805
|
msg = await puppeteerToFile(msg);
|
|
694
806
|
}
|
|
695
807
|
await Promise.all(html('video').map(async (v, i) => videoList.push([await getVideoUrl(i.attribs.src, arg, true, i), (i.attribs.poster && config.basic.usePoster) ? await getImageUrl(i.attribs.poster, arg, true) : ""])));
|
|
696
|
-
|
|
808
|
+
// ★★★ 修复点:过滤掉没有 src 的视频 ★★★
|
|
809
|
+
msg += videoList.filter(([src]) => src).map(([src, poster]) => (0, koishi_1.h)('video', { src, poster })).join("");
|
|
697
810
|
}
|
|
698
811
|
else if (template == "link") {
|
|
699
812
|
html = cheerio.load(item.description);
|
|
@@ -802,8 +915,11 @@ function apply(ctx, config) {
|
|
|
802
915
|
messageList = await Promise.all(rssItemArray.reverse().map(async (i) => await parseRssItem(i, { ...rssItem, ...arg }, rssItem.author)));
|
|
803
916
|
}
|
|
804
917
|
let message;
|
|
805
|
-
if (!messageList.join(""))
|
|
806
|
-
|
|
918
|
+
if (!messageList.join("")) {
|
|
919
|
+
// 如果解析不出内容,也应该更新时间,防止反复解析空内容
|
|
920
|
+
await ctx.database.set('rssOwl', { id: rssItem.id }, { lastPubDate, arg: originalArg, lastContent });
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
807
923
|
if (arg.merge === true) {
|
|
808
924
|
message = `<message forward><author id="${rssItem.author}"/>${messageList.join("")}</message>`;
|
|
809
925
|
}
|
|
@@ -826,11 +942,27 @@ function apply(ctx, config) {
|
|
|
826
942
|
if (rssItem.followers.length) {
|
|
827
943
|
message += `<message>${rssItem.followers.map(followId => `<at ${followId == 'all' ? 'type' : 'id'}='${followId}'/>`)}</message>`;
|
|
828
944
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
945
|
+
// 发送逻辑改进:捕获发送错误,防止死循环
|
|
946
|
+
try {
|
|
947
|
+
const broadcast = await ctx.broadcast([`${rssItem.platform}:${rssItem.guildId}`], message);
|
|
948
|
+
if (!broadcast.length) {
|
|
949
|
+
logger.warn(`RSS [${rssItem.title}] 消息生成成功但未发送给任何目标 (可能群不存在或Bot被禁言)`);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
debug(`更新成功:${rssItem.title}`, '', 'info');
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (sendError) {
|
|
956
|
+
logger.error(`RSS推送失败 [${rssItem.title}]: ${sendError.message}`);
|
|
957
|
+
logger.warn(`已跳过该条 RSS 更新以防止无限重试循环。`);
|
|
958
|
+
}
|
|
959
|
+
// 关键:无论发送成功还是失败,都更新数据库状态,防止死循环
|
|
960
|
+
try {
|
|
961
|
+
await ctx.database.set('rssOwl', { id: rssItem.id }, { lastPubDate, arg: originalArg, lastContent });
|
|
962
|
+
}
|
|
963
|
+
catch (dbError) {
|
|
964
|
+
logger.error(`数据库更新失败: ${dbError}`);
|
|
965
|
+
}
|
|
834
966
|
}
|
|
835
967
|
catch (error) {
|
|
836
968
|
debug(error, `更新失败:${JSON.stringify({ ...rssItem, lastContent: "..." })}`, 'error');
|
package/package.json
CHANGED
package/readme.md
CHANGED