koishi-plugin-video-parser-all 1.0.2 → 1.0.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/lib/index.d.ts +2 -0
- package/lib/index.js +302 -125
- package/package.json +1 -1
- package/readme.md +8 -6
package/lib/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const Config: Schema<{
|
|
|
15
15
|
tempDir?: string | null | undefined;
|
|
16
16
|
maxVideoSize?: number | null | undefined;
|
|
17
17
|
forceDownloadVideo?: boolean | null | undefined;
|
|
18
|
+
videoLoadWaitTime?: number | null | undefined;
|
|
18
19
|
} & {
|
|
19
20
|
timeout?: number | null | undefined;
|
|
20
21
|
videoSendTimeout?: number | null | undefined;
|
|
@@ -46,6 +47,7 @@ export declare const Config: Schema<{
|
|
|
46
47
|
tempDir: string;
|
|
47
48
|
maxVideoSize: number;
|
|
48
49
|
forceDownloadVideo: boolean;
|
|
50
|
+
videoLoadWaitTime: number;
|
|
49
51
|
} & {
|
|
50
52
|
timeout: number;
|
|
51
53
|
videoSendTimeout: number;
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const fs_1 = require("fs");
|
|
13
13
|
const promises_2 = require("stream/promises");
|
|
14
|
+
const lru_cache_1 = require("lru-cache");
|
|
14
15
|
exports.name = 'video-parser-all';
|
|
15
16
|
exports.Config = koishi_1.Schema.intersect([
|
|
16
17
|
koishi_1.Schema.object({
|
|
@@ -25,24 +26,25 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
25
26
|
koishi_1.Schema.object({
|
|
26
27
|
showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
|
|
27
28
|
showVideoFile: koishi_1.Schema.boolean().default(true).description('是否发送视频文件(关闭则只发送视频链接)'),
|
|
28
|
-
maxDescLength: koishi_1.Schema.number().default(200).description('简介内容最大长度(字符),超出自动截断'),
|
|
29
|
-
videoDownloadTimeout: koishi_1.Schema.number().default(120000).description('视频下载超时(毫秒)'),
|
|
29
|
+
maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介内容最大长度(字符),超出自动截断'),
|
|
30
|
+
videoDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('视频下载超时(毫秒)'),
|
|
30
31
|
tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
|
|
31
32
|
maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制大小'),
|
|
32
|
-
forceDownloadVideo: koishi_1.Schema.boolean().default(
|
|
33
|
+
forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频后发送'),
|
|
34
|
+
videoLoadWaitTime: koishi_1.Schema.number().min(0).step(1).default(180000).description('视频链接加载等待时间(毫秒),获取到视频链接后等待指定时间再发送,0为不等待'),
|
|
33
35
|
}).description('内容显示设置'),
|
|
34
36
|
koishi_1.Schema.object({
|
|
35
|
-
timeout: koishi_1.Schema.number().min(0).default(180000).description('API 请求超时(毫秒)'),
|
|
36
|
-
videoSendTimeout: koishi_1.Schema.number().min(0).default(60000).description('视频消息发送超时(毫秒,0 为不限制)'),
|
|
37
|
-
userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36').description('API 请求 UA'),
|
|
37
|
+
timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
|
|
38
|
+
videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('视频消息发送超时(毫秒,0 为不限制)'),
|
|
39
|
+
userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36').description('API 请求 UA'),
|
|
38
40
|
}).description('网络与 API 设置'),
|
|
39
41
|
koishi_1.Schema.object({
|
|
40
42
|
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送失败,避免插件崩溃'),
|
|
41
|
-
retryTimes: koishi_1.Schema.number().min(0).default(3).description('API 请求及消息发送失败时的重试次数'),
|
|
42
|
-
retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔(毫秒,同时用于消息发送重试)'),
|
|
43
|
+
retryTimes: koishi_1.Schema.number().min(0).step(1).default(3).description('API 请求及消息发送失败时的重试次数'),
|
|
44
|
+
retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒,同时用于消息发送重试)'),
|
|
43
45
|
}).description('错误与重试设置'),
|
|
44
46
|
koishi_1.Schema.object({
|
|
45
|
-
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot
|
|
47
|
+
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
|
|
46
48
|
}).description('发送方式设置'),
|
|
47
49
|
koishi_1.Schema.object({
|
|
48
50
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示'),
|
|
@@ -58,39 +60,49 @@ function debugLog(level, ...args) {
|
|
|
58
60
|
if (!debugEnabled)
|
|
59
61
|
return;
|
|
60
62
|
const timestamp = new Date().toISOString();
|
|
61
|
-
const message = `[${timestamp}] [${level}] ${args.map(a =>
|
|
63
|
+
const message = `[${timestamp}] [${level}] ${args.map(a => {
|
|
64
|
+
if (typeof a === 'object') {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(a, null, 2);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return String(a);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return String(a);
|
|
73
|
+
}).join(' ')}`;
|
|
62
74
|
logger.info(message);
|
|
63
75
|
}
|
|
64
76
|
function linkTypeParser(content) {
|
|
65
77
|
content = content.replace(/\\\//g, '/');
|
|
66
78
|
const rules = [
|
|
67
79
|
{ pattern: /bilibili\.com\/video\/([ab]v[0-9a-zA-Z]+)/gi, type: 'bilibili', buildUrl: (id) => `https://www.bilibili.com/video/${id}` },
|
|
68
|
-
{ pattern: /b23\.tv(
|
|
69
|
-
{ pattern: /bili(?:22|23|33)\.cn\/([0-9a-zA-Z]
|
|
70
|
-
{ pattern: /bili2233\.cn\/([0-9a-zA-Z]
|
|
71
|
-
{ pattern: /douyin\.com\/video\/(\d
|
|
72
|
-
{ pattern: /v\.douyin\.com\/([0-9a-zA-Z]
|
|
73
|
-
{ pattern: /kuaishou\.com\/short-video\/([0-9a-zA-Z]
|
|
74
|
-
{ pattern: /v\.kuaishou\.com\/([0-9a-zA-Z]
|
|
75
|
-
{ pattern: /xiaohongshu\.com\/discovery\/item\/([0-9a-zA-Z]
|
|
76
|
-
{ pattern: /xhslink\.com\/([0-9a-zA-Z]
|
|
77
|
-
{ pattern: /weibo\.com\/\d+\/([0-9a-zA-Z]
|
|
78
|
-
{ pattern: /video\.weibo\.com\/show\?fid=([0-9a-zA-Z]
|
|
79
|
-
{ pattern: /ixigua\.com\/(\d
|
|
80
|
-
{ pattern: /youtube\.com\/watch\?v=([a-zA-Z0-9_-]
|
|
81
|
-
{ pattern: /youtu\.be\/([a-zA-Z0-9_-]
|
|
82
|
-
{ pattern: /tiktok\.com\/@[\w.]+\/video\/(\d
|
|
83
|
-
{ pattern: /vm\.tiktok\.com\/([0-9a-zA-Z]
|
|
84
|
-
{ pattern: /acfun\.cn\/v\/(ac\d
|
|
85
|
-
{ pattern: /zhihu\.com\/video\/(\d
|
|
86
|
-
{ pattern: /weishi\.qq\.com\/weishi\/feed\/([0-9a-zA-Z]
|
|
87
|
-
{ pattern: /huya\.com\/video\/([0-9a-zA-Z]
|
|
88
|
-
{ pattern: /haokan\.baidu\.com\/v\?vid=([0-9a-zA-Z]
|
|
89
|
-
{ pattern: /meipai\.com\/media\/(\d
|
|
90
|
-
{ pattern: /twitter\.com\/\w+\/status\/(\d
|
|
91
|
-
{ pattern: /x\.com\/\w+\/status\/(\d
|
|
92
|
-
{ pattern: /instagram\.com\/p\/([0-9a-zA-Z_-]
|
|
93
|
-
{ pattern: /doubao\.com\/video\/(\d
|
|
80
|
+
{ pattern: /b23\.tv\/([0-9a-zA-Z]{5,})/gi, type: 'bilibili', buildUrl: (id) => `https://b23.tv/${id}` },
|
|
81
|
+
{ pattern: /bili(?:22|23|33)\.cn\/([0-9a-zA-Z]{5,})/gi, type: 'bilibili', buildUrl: (id) => `https://bili23.cn/${id}` },
|
|
82
|
+
{ pattern: /bili2233\.cn\/([0-9a-zA-Z]{5,})/gi, type: 'bilibili', buildUrl: (id) => `https://bili2233.cn/${id}` },
|
|
83
|
+
{ pattern: /douyin\.com\/video\/(\d{10,})/gi, type: 'douyin', buildUrl: (id) => `https://www.douyin.com/video/${id}` },
|
|
84
|
+
{ pattern: /v\.douyin\.com\/([0-9a-zA-Z]{8,})/gi, type: 'douyin', buildUrl: (id) => `https://v.douyin.com/${id}/` },
|
|
85
|
+
{ pattern: /kuaishou\.com\/short-video\/([0-9a-zA-Z]{10,})/gi, type: 'kuaishou', buildUrl: (id) => `https://www.kuaishou.com/short-video/${id}` },
|
|
86
|
+
{ pattern: /v\.kuaishou\.com\/([0-9a-zA-Z]{8,})/gi, type: 'kuaishou', buildUrl: (id) => `https://v.kuaishou.com/${id}` },
|
|
87
|
+
{ pattern: /xiaohongshu\.com\/discovery\/item\/([0-9a-zA-Z]{10,})/gi, type: 'xiaohongshu', buildUrl: (id) => `https://www.xiaohongshu.com/discovery/item/${id}` },
|
|
88
|
+
{ pattern: /xhslink\.com\/([0-9a-zA-Z]{8,})/gi, type: 'xiaohongshu', buildUrl: (id) => `https://xhslink.com/${id}` },
|
|
89
|
+
{ pattern: /weibo\.com\/\d+\/([0-9a-zA-Z]{10,})/gi, type: 'weibo', buildUrl: (id) => `https://weibo.com/${id}` },
|
|
90
|
+
{ pattern: /video\.weibo\.com\/show\?fid=([0-9a-zA-Z]{10,})/gi, type: 'weibo', buildUrl: (id) => `https://video.weibo.com/show?fid=${id}` },
|
|
91
|
+
{ pattern: /ixigua\.com\/(\d{10,})/gi, type: 'xigua', buildUrl: (id) => `https://www.ixigua.com/${id}` },
|
|
92
|
+
{ pattern: /youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/gi, type: 'youtube', buildUrl: (id) => `https://www.youtube.com/watch?v=${id}` },
|
|
93
|
+
{ pattern: /youtu\.be\/([a-zA-Z0-9_-]{11})/gi, type: 'youtube', buildUrl: (id) => `https://youtu.be/${id}` },
|
|
94
|
+
{ pattern: /tiktok\.com\/@[\w.]+\/video\/(\d{10,})/gi, type: 'tiktok', buildUrl: (id) => `https://www.tiktok.com/@user/video/${id}` },
|
|
95
|
+
{ pattern: /vm\.tiktok\.com\/([0-9a-zA-Z]{8,})/gi, type: 'tiktok', buildUrl: (id) => `https://vm.tiktok.com/${id}` },
|
|
96
|
+
{ pattern: /acfun\.cn\/v\/(ac\d{10,})/gi, type: 'acfun', buildUrl: (id) => `https://www.acfun.cn/v/${id}` },
|
|
97
|
+
{ pattern: /zhihu\.com\/video\/(\d{10,})/gi, type: 'zhihu', buildUrl: (id) => `https://www.zhihu.com/video/${id}` },
|
|
98
|
+
{ pattern: /weishi\.qq\.com\/weishi\/feed\/([0-9a-zA-Z]{10,})/gi, type: 'weishi', buildUrl: (id) => `https://weishi.qq.com/weishi/feed/${id}` },
|
|
99
|
+
{ pattern: /huya\.com\/video\/([0-9a-zA-Z]{10,})/gi, type: 'huya', buildUrl: (id) => `https://www.huya.com/video/${id}` },
|
|
100
|
+
{ pattern: /haokan\.baidu\.com\/v\?vid=([0-9a-zA-Z]{10,})/gi, type: 'haokan', buildUrl: (id) => `https://haokan.baidu.com/v?vid=${id}` },
|
|
101
|
+
{ pattern: /meipai\.com\/media\/(\d{10,})/gi, type: 'meipai', buildUrl: (id) => `https://www.meipai.com/media/${id}` },
|
|
102
|
+
{ pattern: /twitter\.com\/\w+\/status\/(\d{10,})/gi, type: 'twitter', buildUrl: (id) => `https://twitter.com/i/status/${id}` },
|
|
103
|
+
{ pattern: /x\.com\/\w+\/status\/(\d{10,})/gi, type: 'twitter', buildUrl: (id) => `https://x.com/i/status/${id}` },
|
|
104
|
+
{ pattern: /instagram\.com\/p\/([0-9a-zA-Z_-]{10,})/gi, type: 'instagram', buildUrl: (id) => `https://www.instagram.com/p/${id}` },
|
|
105
|
+
{ pattern: /doubao\.com\/video\/(\d{10,})/gi, type: 'doubao', buildUrl: (id) => `https://www.doubao.com/video/${id}` },
|
|
94
106
|
];
|
|
95
107
|
const matches = [];
|
|
96
108
|
const seen = new Set();
|
|
@@ -108,17 +120,30 @@ function linkTypeParser(content) {
|
|
|
108
120
|
return matches;
|
|
109
121
|
}
|
|
110
122
|
function extractUrl(content) {
|
|
111
|
-
|
|
123
|
+
if (!content)
|
|
124
|
+
return [];
|
|
125
|
+
const urlMatches = content.match(/https?:\/\/[^\s<>"'(){}[\]]+/gi) || [];
|
|
112
126
|
return urlMatches.filter(url => {
|
|
113
127
|
try {
|
|
114
|
-
const
|
|
115
|
-
|
|
128
|
+
const urlObj = new URL(url);
|
|
129
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
130
|
+
if (hostname.includes('multimedia.nt.qq.com.cn') ||
|
|
131
|
+
hostname.includes('grouptalk.qq.com') ||
|
|
132
|
+
hostname.includes('qpic.cn') ||
|
|
133
|
+
hostname.includes('qlogo.cn')) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (hostname === 'v.douyin.com' && urlObj.pathname.length < 3)
|
|
137
|
+
return false;
|
|
138
|
+
if (hostname === 'www.douyin.com' && urlObj.pathname === '/')
|
|
116
139
|
return false;
|
|
117
140
|
return true;
|
|
118
141
|
}
|
|
119
142
|
catch {
|
|
120
143
|
return false;
|
|
121
144
|
}
|
|
145
|
+
}).map(url => {
|
|
146
|
+
return url.replace(/[.,;:!?)]+$/, '');
|
|
122
147
|
});
|
|
123
148
|
}
|
|
124
149
|
function extractAllUrlsFromMessage(session) {
|
|
@@ -138,10 +163,11 @@ function extractAllUrlsFromMessage(session) {
|
|
|
138
163
|
if (session.elements) {
|
|
139
164
|
for (const elem of session.elements) {
|
|
140
165
|
if (elem.type === 'xml' && elem.data) {
|
|
141
|
-
const urlRegex = /https?:\/\/[^\s<>"']+/gi;
|
|
166
|
+
const urlRegex = /https?:\/\/[^\s<>"'(){}[\]]+/gi;
|
|
142
167
|
let match;
|
|
143
168
|
while ((match = urlRegex.exec(elem.data)) !== null) {
|
|
144
|
-
|
|
169
|
+
const cleanUrl = match[0].replace(/[.,;:!?)]+$/, '');
|
|
170
|
+
urls.push(cleanUrl);
|
|
145
171
|
}
|
|
146
172
|
}
|
|
147
173
|
else if (elem.type === 'json' && elem.data) {
|
|
@@ -152,9 +178,13 @@ function extractAllUrlsFromMessage(session) {
|
|
|
152
178
|
return;
|
|
153
179
|
for (const val of Object.values(obj)) {
|
|
154
180
|
if (typeof val === 'string') {
|
|
155
|
-
const match = val.match(/https?:\/\/[^\s<>"']+/gi);
|
|
156
|
-
if (match)
|
|
157
|
-
|
|
181
|
+
const match = val.match(/https?:\/\/[^\s<>"'(){}[\]]+/gi);
|
|
182
|
+
if (match) {
|
|
183
|
+
match.forEach(url => {
|
|
184
|
+
const cleanUrl = url.replace(/[.,;:!?)]+$/, '');
|
|
185
|
+
urls.push(cleanUrl);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
158
188
|
}
|
|
159
189
|
else if (typeof val === 'object')
|
|
160
190
|
extractFromObject(val);
|
|
@@ -162,24 +192,49 @@ function extractAllUrlsFromMessage(session) {
|
|
|
162
192
|
};
|
|
163
193
|
extractFromObject(json);
|
|
164
194
|
}
|
|
165
|
-
catch {
|
|
195
|
+
catch (e) {
|
|
196
|
+
debugLog('WARN', '解析JSON卡片失败:', e);
|
|
197
|
+
}
|
|
166
198
|
}
|
|
167
199
|
}
|
|
168
200
|
}
|
|
169
|
-
return [...new Set(urls)]
|
|
201
|
+
return [...new Set(urls)].filter(url => {
|
|
202
|
+
try {
|
|
203
|
+
const urlObj = new URL(url);
|
|
204
|
+
if (urlObj.hostname === 'v.douyin.com' && urlObj.pathname.length < 3)
|
|
205
|
+
return false;
|
|
206
|
+
if (urlObj.hostname === 'www.douyin.com' && urlObj.pathname === '/')
|
|
207
|
+
return false;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
170
214
|
}
|
|
171
215
|
function cleanUrl(url) {
|
|
172
216
|
try {
|
|
173
217
|
url = url.replace(/&/g, '&');
|
|
174
218
|
const urlObj = new URL(url);
|
|
219
|
+
if (urlObj.protocol === 'http:') {
|
|
220
|
+
urlObj.protocol = 'https:';
|
|
221
|
+
}
|
|
175
222
|
if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
|
|
176
|
-
|
|
177
|
-
|
|
223
|
+
['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => {
|
|
224
|
+
urlObj.searchParams.delete(p);
|
|
225
|
+
});
|
|
226
|
+
return urlObj.origin + urlObj.pathname;
|
|
227
|
+
}
|
|
228
|
+
if (urlObj.hostname.includes('bilibili.com') || urlObj.hostname.includes('b23.tv')) {
|
|
229
|
+
['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p => {
|
|
230
|
+
urlObj.searchParams.delete(p);
|
|
231
|
+
});
|
|
178
232
|
return urlObj.origin + urlObj.pathname;
|
|
179
233
|
}
|
|
180
|
-
return
|
|
234
|
+
return urlObj.toString();
|
|
181
235
|
}
|
|
182
236
|
catch (e) {
|
|
237
|
+
debugLog('WARN', '清理URL失败:', e, '原始URL:', url);
|
|
183
238
|
return url.replace(/&/g, '&').replace(/\?.*/, '');
|
|
184
239
|
}
|
|
185
240
|
}
|
|
@@ -198,6 +253,7 @@ async function resolveShortUrl(url) {
|
|
|
198
253
|
return cleanUrl(finalUrl);
|
|
199
254
|
}
|
|
200
255
|
catch (e) {
|
|
256
|
+
debugLog('WARN', '解析短链接失败:', e, '原始URL:', url);
|
|
201
257
|
return cleanUrl(url);
|
|
202
258
|
}
|
|
203
259
|
}
|
|
@@ -209,7 +265,7 @@ function formatDuration(seconds) {
|
|
|
209
265
|
const s = Math.floor(seconds % 60);
|
|
210
266
|
if (h > 0)
|
|
211
267
|
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
212
|
-
return `${m
|
|
268
|
+
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
213
269
|
}
|
|
214
270
|
function formatPublishTime(ms) {
|
|
215
271
|
if (!ms)
|
|
@@ -222,8 +278,13 @@ function pickBestQuality(videoBackup) {
|
|
|
222
278
|
if (!Array.isArray(videoBackup))
|
|
223
279
|
return [];
|
|
224
280
|
return videoBackup
|
|
225
|
-
.
|
|
226
|
-
.
|
|
281
|
+
.filter(v => v && v.url)
|
|
282
|
+
.map(v => ({
|
|
283
|
+
quality: v.quality || v.label || 'unknown',
|
|
284
|
+
url: v.url,
|
|
285
|
+
bit_rate: Number(v.bit_rate || 0)
|
|
286
|
+
}))
|
|
287
|
+
.sort((a, b) => b.bit_rate - a.bit_rate);
|
|
227
288
|
}
|
|
228
289
|
function parseApiResponse(raw, maxDescLen) {
|
|
229
290
|
debugLog('DEBUG', '原始API返回数据:', raw);
|
|
@@ -253,24 +314,44 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
253
314
|
avatar = data.avatar || '';
|
|
254
315
|
}
|
|
255
316
|
const title = data.title || '';
|
|
256
|
-
const desc = (data.desc || data.description || '').slice(0, maxDescLen);
|
|
317
|
+
const desc = (data.desc || data.description || '').slice(0, maxDescLen).trim();
|
|
257
318
|
const cover = data.cover || '';
|
|
258
319
|
let video = '';
|
|
259
320
|
let videos = [];
|
|
260
321
|
if (Array.isArray(data.video_backup) && data.video_backup.length) {
|
|
261
322
|
const bestQ = pickBestQuality(data.video_backup);
|
|
262
323
|
videos = bestQ;
|
|
263
|
-
video = bestQ[0]?.url ||
|
|
324
|
+
video = bestQ[0]?.url || '';
|
|
264
325
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
326
|
+
if (!video && Array.isArray(data.videos) && data.videos.length) {
|
|
327
|
+
const validVideos = data.videos.filter((v) => v && v.url);
|
|
328
|
+
if (validVideos.length) {
|
|
329
|
+
video = validVideos[0].url;
|
|
330
|
+
videos = validVideos.map((v) => ({
|
|
331
|
+
quality: v.accept?.[0] || 'unknown',
|
|
332
|
+
url: v.url
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
268
335
|
}
|
|
269
|
-
|
|
270
|
-
video = data.url
|
|
336
|
+
if (!video && data.url) {
|
|
337
|
+
video = data.url;
|
|
338
|
+
}
|
|
339
|
+
if (video && !video.startsWith('http')) {
|
|
340
|
+
video = 'https:' + video;
|
|
271
341
|
}
|
|
272
|
-
const images = Array.isArray(data.images)
|
|
273
|
-
|
|
342
|
+
const images = Array.isArray(data.images)
|
|
343
|
+
? data.images.filter((img) => img && typeof img === 'string').map((img) => {
|
|
344
|
+
if (!img.startsWith('http'))
|
|
345
|
+
return 'https:' + img;
|
|
346
|
+
return img;
|
|
347
|
+
})
|
|
348
|
+
: [];
|
|
349
|
+
const live_photo = Array.isArray(data.live_photo)
|
|
350
|
+
? data.live_photo.filter((lp) => lp && lp.image).map((lp) => ({
|
|
351
|
+
image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
|
|
352
|
+
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
353
|
+
}))
|
|
354
|
+
: [];
|
|
274
355
|
const music = {
|
|
275
356
|
title: data.music?.title || data.music?.name || '',
|
|
276
357
|
author: data.music?.author || data.music?.artist || '',
|
|
@@ -301,6 +382,10 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
301
382
|
else if (extra.create_time) {
|
|
302
383
|
publishTime = extra.create_time * 1000;
|
|
303
384
|
}
|
|
385
|
+
debugLog('DEBUG', '解析后的数据:', {
|
|
386
|
+
type, title, author, video: video.substring(0, 100) + '...',
|
|
387
|
+
images: images.length, live_photo: live_photo.length
|
|
388
|
+
});
|
|
304
389
|
return {
|
|
305
390
|
type, title, desc, author, uid, avatar, cover,
|
|
306
391
|
video, videos, images, live_photo, music,
|
|
@@ -324,6 +409,7 @@ function generateFormattedText(p, format) {
|
|
|
324
409
|
'图片数量': String(imageCount),
|
|
325
410
|
'作者ID': p.uid,
|
|
326
411
|
'封面': p.cover,
|
|
412
|
+
'视频链接': p.video,
|
|
327
413
|
};
|
|
328
414
|
const lines = format.split('\n');
|
|
329
415
|
const resultLines = [];
|
|
@@ -359,24 +445,46 @@ function buildForwardNode(session, content, botName) {
|
|
|
359
445
|
messageContent = [content];
|
|
360
446
|
else
|
|
361
447
|
messageContent = [koishi_1.h.text(String(content))];
|
|
362
|
-
return (0, koishi_1.h)('node', {
|
|
448
|
+
return (0, koishi_1.h)('node', {
|
|
449
|
+
user: {
|
|
450
|
+
nickname: botName.substring(0, 15),
|
|
451
|
+
user_id: session.selfId
|
|
452
|
+
}
|
|
453
|
+
}, messageContent);
|
|
363
454
|
}
|
|
364
|
-
const urlCache = new
|
|
365
|
-
|
|
455
|
+
const urlCache = new lru_cache_1.LRUCache({
|
|
456
|
+
max: 500,
|
|
457
|
+
ttl: 10 * 60 * 1000,
|
|
458
|
+
updateAgeOnGet: false,
|
|
459
|
+
});
|
|
366
460
|
async function downloadVideoFile(videoUrl, tempDir, timeout, maxSizeMB) {
|
|
461
|
+
if (!videoUrl)
|
|
462
|
+
throw new Error('视频链接为空');
|
|
367
463
|
await promises_1.default.mkdir(tempDir, { recursive: true });
|
|
368
464
|
const fileName = `video_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.mp4`;
|
|
369
|
-
const filePath = path_1.default.
|
|
465
|
+
const filePath = path_1.default.resolve(tempDir, fileName);
|
|
466
|
+
debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
|
|
467
|
+
debugLog('INFO', `临时文件路径: ${filePath}`);
|
|
370
468
|
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
469
|
+
let response;
|
|
470
|
+
try {
|
|
471
|
+
response = await (0, axios_1.default)({
|
|
472
|
+
method: 'GET',
|
|
473
|
+
url: videoUrl,
|
|
474
|
+
responseType: 'stream',
|
|
475
|
+
timeout: timeout,
|
|
476
|
+
headers: {
|
|
477
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
478
|
+
'Referer': 'https://www.bilibili.com/',
|
|
479
|
+
},
|
|
480
|
+
validateStatus: (status) => status >= 200 && status < 300,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
writer.destroy();
|
|
485
|
+
await promises_1.default.unlink(filePath).catch(() => { });
|
|
486
|
+
throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
|
|
487
|
+
}
|
|
380
488
|
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
381
489
|
const contentLength = Number(response.headers['content-length'] || 0);
|
|
382
490
|
if (maxSizeMB > 0 && contentLength > maxSizeBytes) {
|
|
@@ -394,8 +502,15 @@ async function downloadVideoFile(videoUrl, tempDir, timeout, maxSizeMB) {
|
|
|
394
502
|
throw new Error(`视频文件过大,超过限制(${maxSizeMB}MB)`);
|
|
395
503
|
}
|
|
396
504
|
});
|
|
397
|
-
|
|
398
|
-
|
|
505
|
+
try {
|
|
506
|
+
await (0, promises_2.pipeline)(response.data, writer);
|
|
507
|
+
debugLog('INFO', `视频下载完成,大小: ${Math.round(downloadedSize / 1024 / 1024)}MB`);
|
|
508
|
+
return filePath;
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
await promises_1.default.unlink(filePath).catch(() => { });
|
|
512
|
+
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
513
|
+
}
|
|
399
514
|
}
|
|
400
515
|
function getErrorMessage(error) {
|
|
401
516
|
if (error instanceof Error)
|
|
@@ -411,7 +526,9 @@ function isSpecialPlatformVideo(url) {
|
|
|
411
526
|
'xhslink.com',
|
|
412
527
|
'zhihu.com',
|
|
413
528
|
'weibo.com',
|
|
414
|
-
'sinaimg.cn'
|
|
529
|
+
'sinaimg.cn',
|
|
530
|
+
'ixigua.com',
|
|
531
|
+
'toutiao.com',
|
|
415
532
|
];
|
|
416
533
|
return specialHosts.some(host => url.includes(host));
|
|
417
534
|
}
|
|
@@ -428,7 +545,7 @@ function apply(ctx, config) {
|
|
|
428
545
|
const http = axios_1.default.create({
|
|
429
546
|
timeout: config.timeout,
|
|
430
547
|
headers: {
|
|
431
|
-
'User-Agent': config.userAgent
|
|
548
|
+
'User-Agent': config.userAgent,
|
|
432
549
|
'Referer': 'https://www.baidu.com/',
|
|
433
550
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
434
551
|
}
|
|
@@ -448,13 +565,16 @@ function apply(ctx, config) {
|
|
|
448
565
|
params: { url },
|
|
449
566
|
timeout: config.timeout
|
|
450
567
|
});
|
|
451
|
-
debugLog('DEBUG', `API
|
|
568
|
+
debugLog('DEBUG', `API响应状态: ${res.status}`);
|
|
452
569
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
453
570
|
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
454
|
-
urlCache.set(cacheKey, {
|
|
571
|
+
urlCache.set(cacheKey, {
|
|
572
|
+
data: parsed,
|
|
573
|
+
expire: Date.now() + 10 * 60 * 1000
|
|
574
|
+
});
|
|
455
575
|
return parsed;
|
|
456
576
|
}
|
|
457
|
-
throw new Error(res.data?.msg ||
|
|
577
|
+
throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
|
|
458
578
|
}
|
|
459
579
|
catch (error) {
|
|
460
580
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -472,7 +592,10 @@ function apply(ctx, config) {
|
|
|
472
592
|
for (const candidate of [...new Set(candidates)]) {
|
|
473
593
|
try {
|
|
474
594
|
const info = await fetchApi(candidate);
|
|
475
|
-
|
|
595
|
+
if (info.video || info.images.length > 0) {
|
|
596
|
+
return { success: true, data: info };
|
|
597
|
+
}
|
|
598
|
+
debugLog('WARN', `解析成功但无有效内容: ${candidate}`);
|
|
476
599
|
}
|
|
477
600
|
catch (error) {
|
|
478
601
|
debugLog('ERROR', `候选链接解析失败: ${candidate}`, getErrorMessage(error));
|
|
@@ -482,10 +605,17 @@ function apply(ctx, config) {
|
|
|
482
605
|
}
|
|
483
606
|
async function processSingleUrl(url) {
|
|
484
607
|
const result = await parseUrl(url);
|
|
485
|
-
if (!result.success)
|
|
486
|
-
return result;
|
|
608
|
+
if (!result.success) {
|
|
609
|
+
return { success: false, msg: result.msg, url };
|
|
610
|
+
}
|
|
487
611
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
488
|
-
return {
|
|
612
|
+
return {
|
|
613
|
+
success: true,
|
|
614
|
+
data: {
|
|
615
|
+
text,
|
|
616
|
+
parsed: result.data
|
|
617
|
+
}
|
|
618
|
+
};
|
|
489
619
|
}
|
|
490
620
|
async function sendWithTimeout(session, content, customRetries) {
|
|
491
621
|
const maxRetries = customRetries ?? config.retryTimes ?? 3;
|
|
@@ -528,24 +658,34 @@ function apply(ctx, config) {
|
|
|
528
658
|
async function sendVideoFile(session, videoUrl) {
|
|
529
659
|
if (!videoUrl)
|
|
530
660
|
throw new Error('视频链接为空');
|
|
531
|
-
|
|
532
|
-
|
|
661
|
+
if (config.videoLoadWaitTime > 0) {
|
|
662
|
+
await delay(config.videoLoadWaitTime);
|
|
663
|
+
}
|
|
664
|
+
await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { });
|
|
665
|
+
if (!config.showVideoFile) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
if (!config.forceDownloadVideo) {
|
|
533
669
|
try {
|
|
534
670
|
debugLog('INFO', `尝试直接发送视频URL: ${videoUrl.substring(0, 100)}...`);
|
|
535
|
-
|
|
671
|
+
const result = await sendWithTimeout(session, koishi_1.h.video(videoUrl));
|
|
672
|
+
if (result) {
|
|
673
|
+
debugLog('INFO', '直接发送视频URL成功');
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
536
676
|
}
|
|
537
677
|
catch (err) {
|
|
538
678
|
debugLog('ERROR', `直接发送URL失败,开始下载视频: ${getErrorMessage(err)}`);
|
|
539
679
|
}
|
|
540
680
|
}
|
|
541
681
|
else {
|
|
542
|
-
debugLog('INFO',
|
|
682
|
+
debugLog('INFO', `强制下载视频后发送: ${videoUrl.substring(0, 100)}...`);
|
|
543
683
|
}
|
|
544
684
|
let tempFilePath = null;
|
|
545
685
|
try {
|
|
546
686
|
tempFilePath = await downloadVideoFile(videoUrl, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
|
|
547
|
-
const localFile = `file://${
|
|
548
|
-
debugLog('INFO',
|
|
687
|
+
const localFile = `file://${tempFilePath}`;
|
|
688
|
+
debugLog('INFO', `发送本地视频文件: ${localFile}`);
|
|
549
689
|
return await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
550
690
|
}
|
|
551
691
|
finally {
|
|
@@ -556,40 +696,39 @@ function apply(ctx, config) {
|
|
|
556
696
|
}
|
|
557
697
|
async function flush(session, urls) {
|
|
558
698
|
const uniqueUrls = [...new Set(urls)];
|
|
699
|
+
debugLog('INFO', `开始解析 ${uniqueUrls.length} 个链接`);
|
|
559
700
|
const items = [];
|
|
560
701
|
const errors = [];
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
|
|
577
|
-
.replace(/\$\{msg\}/g, res.msg);
|
|
578
|
-
errors.push(item);
|
|
579
|
-
}
|
|
702
|
+
for (let i = 0; i < uniqueUrls.length; i++) {
|
|
703
|
+
const url = uniqueUrls[i];
|
|
704
|
+
debugLog('INFO', `正在解析第 ${i + 1}/${uniqueUrls.length} 个链接: ${url}`);
|
|
705
|
+
const result = await processSingleUrl(url);
|
|
706
|
+
if (result.success) {
|
|
707
|
+
items.push(result.data);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const item = texts.parseErrorItemFormat
|
|
711
|
+
.replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
|
|
712
|
+
.replace(/\$\{msg\}/g, result.msg);
|
|
713
|
+
errors.push(item);
|
|
714
|
+
}
|
|
715
|
+
if (i < uniqueUrls.length - 1) {
|
|
716
|
+
await delay(500);
|
|
580
717
|
}
|
|
581
718
|
}
|
|
582
719
|
if (errors.length) {
|
|
583
720
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
584
721
|
await delay(500);
|
|
585
722
|
}
|
|
586
|
-
if (!items.length)
|
|
723
|
+
if (!items.length) {
|
|
724
|
+
debugLog('INFO', '没有成功解析的内容');
|
|
587
725
|
return;
|
|
726
|
+
}
|
|
588
727
|
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
589
728
|
const botName = config.botName || '视频解析机器人';
|
|
590
|
-
const videoItems = [];
|
|
591
729
|
if (enableForward) {
|
|
592
730
|
const forwardMessages = [];
|
|
731
|
+
const videoItems = [];
|
|
593
732
|
for (const item of items) {
|
|
594
733
|
const p = item.parsed;
|
|
595
734
|
const text = item.text;
|
|
@@ -606,12 +745,14 @@ function apply(ctx, config) {
|
|
|
606
745
|
}
|
|
607
746
|
}
|
|
608
747
|
if (p.video && config.showVideoFile && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
748
|
+
forwardMessages.push(buildForwardNode(session, `视频链接:${p.video}`, botName));
|
|
609
749
|
videoItems.push(p);
|
|
610
750
|
}
|
|
611
751
|
}
|
|
612
752
|
if (forwardMessages.length) {
|
|
613
753
|
const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
|
|
614
754
|
try {
|
|
755
|
+
debugLog('INFO', `发送合并转发消息,包含 ${forwardMessages.length} 条内容`);
|
|
615
756
|
await sendWithTimeout(session, forwardMsg, config.retryTimes);
|
|
616
757
|
}
|
|
617
758
|
catch (err) {
|
|
@@ -627,8 +768,7 @@ function apply(ctx, config) {
|
|
|
627
768
|
await sendVideoFile(session, p.video);
|
|
628
769
|
}
|
|
629
770
|
catch (err) {
|
|
630
|
-
debugLog('ERROR',
|
|
631
|
-
await sendWithTimeout(session, `视频链接:${p.video}`).catch(() => { });
|
|
771
|
+
debugLog('ERROR', `视频发送失败: ${getErrorMessage(err)}`);
|
|
632
772
|
}
|
|
633
773
|
await delay(500);
|
|
634
774
|
}
|
|
@@ -645,13 +785,12 @@ function apply(ctx, config) {
|
|
|
645
785
|
await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
|
|
646
786
|
await delay(300);
|
|
647
787
|
}
|
|
648
|
-
if (p.video &&
|
|
788
|
+
if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
649
789
|
try {
|
|
650
790
|
await sendVideoFile(session, p.video);
|
|
651
791
|
}
|
|
652
792
|
catch (err) {
|
|
653
|
-
debugLog('ERROR',
|
|
654
|
-
await sendWithTimeout(session, `视频链接:${p.video}`).catch(() => { });
|
|
793
|
+
debugLog('ERROR', `视频发送失败: ${getErrorMessage(err)}`);
|
|
655
794
|
}
|
|
656
795
|
await delay(500);
|
|
657
796
|
}
|
|
@@ -664,41 +803,78 @@ function apply(ctx, config) {
|
|
|
664
803
|
}
|
|
665
804
|
}
|
|
666
805
|
}
|
|
806
|
+
debugLog('INFO', '所有内容处理完成');
|
|
667
807
|
}
|
|
668
808
|
ctx.on('message', async (session) => {
|
|
669
809
|
if (!config.enable)
|
|
670
810
|
return;
|
|
671
|
-
// 修复:使用正确的小写subtype属性名
|
|
672
811
|
if (session.subtype === 'file_upload')
|
|
673
812
|
return;
|
|
674
813
|
if (session.elements?.some(elem => elem.type === 'file' || elem.type === 'folder'))
|
|
675
814
|
return;
|
|
815
|
+
if (session.selfId === session.userId)
|
|
816
|
+
return;
|
|
676
817
|
const urls = extractAllUrlsFromMessage(session);
|
|
677
818
|
if (!urls.length)
|
|
678
819
|
return;
|
|
820
|
+
debugLog('INFO', `检测到 ${urls.length} 个链接,开始处理`);
|
|
679
821
|
if (config.showWaitingTip) {
|
|
680
822
|
try {
|
|
681
823
|
await sendWithTimeout(session, texts.waitingTipText);
|
|
682
824
|
}
|
|
683
|
-
catch {
|
|
825
|
+
catch (e) {
|
|
826
|
+
debugLog('WARN', '发送等待提示失败:', e);
|
|
827
|
+
}
|
|
684
828
|
}
|
|
685
829
|
await flush(session, urls);
|
|
686
830
|
});
|
|
687
831
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
832
|
+
if (!url) {
|
|
833
|
+
await sendWithTimeout(session, texts.invalidLinkText);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
688
836
|
const us = extractUrl(url);
|
|
689
837
|
if (!us.length) {
|
|
690
838
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
691
839
|
return;
|
|
692
840
|
}
|
|
841
|
+
if (config.showWaitingTip) {
|
|
842
|
+
try {
|
|
843
|
+
await sendWithTimeout(session, texts.waitingTipText);
|
|
844
|
+
}
|
|
845
|
+
catch { }
|
|
846
|
+
}
|
|
693
847
|
await flush(session, us);
|
|
694
848
|
});
|
|
695
|
-
setInterval(() => {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
849
|
+
const tempCleanupInterval = setInterval(async () => {
|
|
850
|
+
try {
|
|
851
|
+
const tempDir = config.tempDir || './temp_videos';
|
|
852
|
+
const files = await promises_1.default.readdir(tempDir);
|
|
853
|
+
const now = Date.now();
|
|
854
|
+
let deletedCount = 0;
|
|
855
|
+
for (const file of files) {
|
|
856
|
+
if (file.startsWith('video_') && file.endsWith('.mp4')) {
|
|
857
|
+
const filePath = path_1.default.join(tempDir, file);
|
|
858
|
+
const stats = await promises_1.default.stat(filePath);
|
|
859
|
+
if (now - stats.mtimeMs > 3600000) {
|
|
860
|
+
await promises_1.default.unlink(filePath).catch(() => { });
|
|
861
|
+
deletedCount++;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (deletedCount > 0) {
|
|
866
|
+
debugLog('INFO', `清理了 ${deletedCount} 个过期临时视频文件`);
|
|
867
|
+
}
|
|
700
868
|
}
|
|
701
|
-
|
|
869
|
+
catch (e) {
|
|
870
|
+
debugLog('WARN', '清理临时文件失败:', e);
|
|
871
|
+
}
|
|
872
|
+
}, 3600000);
|
|
873
|
+
ctx.on('dispose', () => {
|
|
874
|
+
clearInterval(tempCleanupInterval);
|
|
875
|
+
urlCache.clear();
|
|
876
|
+
debugLog('INFO', '插件已卸载,资源已清理');
|
|
877
|
+
});
|
|
702
878
|
process.on('exit', async () => {
|
|
703
879
|
try {
|
|
704
880
|
const tempDir = config.tempDir || './temp_videos';
|
|
@@ -708,6 +884,7 @@ function apply(ctx, config) {
|
|
|
708
884
|
await promises_1.default.unlink(path_1.default.join(tempDir, file)).catch(() => { });
|
|
709
885
|
}
|
|
710
886
|
}
|
|
887
|
+
debugLog('INFO', '进程退出,已清理所有临时视频文件');
|
|
711
888
|
}
|
|
712
889
|
catch { }
|
|
713
890
|
});
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
- 📤 支持OneBot平台消息合并转发,优化多图文展示体验
|
|
12
12
|
- 💬 所有提示文案均可自定义,适配多语言场景
|
|
13
13
|
- 🔁 消息发送支持自动重试,与API重试配置联动,增强稳定性
|
|
14
|
-
- 🚀
|
|
15
|
-
- ⚡
|
|
14
|
+
- 🚀 内置LRU内存缓存,避免短时间内重复解析同一链接;串行解析防止API限流
|
|
15
|
+
- ⚡ 智能视频发送策略:优先直接发送URL,失败自动降级为本地文件发送
|
|
16
16
|
- 🛡️ 可选视频大小限制,防止超大文件占满服务器磁盘;自动清理所有临时文件
|
|
17
17
|
|
|
18
18
|
### English
|
|
@@ -24,8 +24,8 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
24
24
|
- 📤 Support OneBot message forwarding for better image/video display
|
|
25
25
|
- 💬 All prompt texts are customizable for multilingual scenarios
|
|
26
26
|
- 🔁 Message sending supports automatic retries, linked with API retry configuration for improved stability
|
|
27
|
-
- 🚀 Built-in memory cache to avoid repeated parsing of the same URL;
|
|
28
|
-
- ⚡ Smart video sending strategy: priority to send URL directly
|
|
27
|
+
- 🚀 Built-in LRU memory cache to avoid repeated parsing of the same URL; serial parsing to prevent API rate limiting
|
|
28
|
+
- ⚡ Smart video sending strategy: priority to send URL directly, auto downgrade to local file on failure
|
|
29
29
|
- 🛡️ Optional video size limit to prevent oversized files from filling up server disk; automatic cleanup of all temporary files
|
|
30
30
|
|
|
31
31
|
## 项目仓库 (Repository)
|
|
@@ -62,14 +62,15 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
62
62
|
| `videoDownloadTimeout` | number | 120000 | 视频下载超时(毫秒) |
|
|
63
63
|
| `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
|
|
64
64
|
| `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 为不限制大小 |
|
|
65
|
-
| `forceDownloadVideo` | boolean |
|
|
65
|
+
| `forceDownloadVideo` | boolean | false | 强制下载视频后发送 |
|
|
66
|
+
| `videoLoadWaitTime` | number | 180000 | 视频链接加载等待时间(毫秒),获取到视频链接后等待指定时间再发送,0为不等待 |
|
|
66
67
|
|
|
67
68
|
### 网络与 API 设置
|
|
68
69
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
69
70
|
|--------|------|--------|------|
|
|
70
71
|
| `timeout` | number | 180000 | API 请求超时时间(毫秒) |
|
|
71
72
|
| `videoSendTimeout` | number | 60000 | 视频消息发送超时时间(毫秒,0 为不限制) |
|
|
72
|
-
| `userAgent` | string | `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36` | API 请求使用的 User-Agent |
|
|
73
|
+
| `userAgent` | string | `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36` | API 请求使用的 User-Agent |
|
|
73
74
|
|
|
74
75
|
### 错误与重试设置
|
|
75
76
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -110,6 +111,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
110
111
|
| `${图片数量}` | 图集/实况图片数量 | 图集/实况 |
|
|
111
112
|
| `${作者ID}` | 作者唯一标识ID | 部分平台 |
|
|
112
113
|
| `${封面}` | 封面图片地址 | 所有平台 |
|
|
114
|
+
| `${视频链接}` | 视频原始链接 | 视频 |
|
|
113
115
|
|
|
114
116
|
> 注:部分变量可能因平台API返回数据不同而显示为空,某行所有变量为空(或为"0")时该行会自动隐藏。
|
|
115
117
|
|