koishi-plugin-video-parser-all 1.5.5 → 1.5.7
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 +4 -20
- package/lib/index.js +85 -367
- package/package.json +6 -29
- package/readme.md +8 -30
package/lib/index.d.ts
CHANGED
|
@@ -48,21 +48,12 @@ export declare const Config: Schema<{
|
|
|
48
48
|
showMusicCover?: boolean | null | undefined;
|
|
49
49
|
showVideoFile?: boolean | null | undefined;
|
|
50
50
|
sendLiveMessage?: boolean | null | undefined;
|
|
51
|
-
forceDownloadCover?: boolean | null | undefined;
|
|
52
|
-
forceDownloadImageNew?: boolean | null | undefined;
|
|
53
|
-
forceDownloadAuthorAvatar?: boolean | null | undefined;
|
|
54
|
-
forceDownloadVideo?: boolean | null | undefined;
|
|
55
51
|
} & {
|
|
56
52
|
showMusicVoice?: boolean | null | undefined;
|
|
57
53
|
showMusicVoiceFile?: boolean | null | undefined;
|
|
58
|
-
forceDownloadMusicVoice?: boolean | null | undefined;
|
|
59
54
|
} & {
|
|
60
55
|
maxDescLength?: number | null | undefined;
|
|
61
56
|
maxConcurrent?: number | null | undefined;
|
|
62
|
-
downloadConcurrency?: number | null | undefined;
|
|
63
|
-
mediaDownloadTimeout?: number | null | undefined;
|
|
64
|
-
maxMediaSize?: number | null | undefined;
|
|
65
|
-
downloadEngine?: "internal" | "aria2" | "downloads" | null | undefined;
|
|
66
57
|
} & {
|
|
67
58
|
timeout?: number | null | undefined;
|
|
68
59
|
videoSendTimeout?: number | null | undefined;
|
|
@@ -88,8 +79,8 @@ export declare const Config: Schema<{
|
|
|
88
79
|
enableForward?: boolean | null | undefined;
|
|
89
80
|
} & {
|
|
90
81
|
deduplicationInterval?: number | null | undefined;
|
|
82
|
+
enableDeduplication?: boolean | null | undefined;
|
|
91
83
|
cacheTTL?: number | null | undefined;
|
|
92
|
-
cacheDir?: string | null | undefined;
|
|
93
84
|
} & {
|
|
94
85
|
primaryApiUrl?: string | null | undefined;
|
|
95
86
|
backupApiUrl?: string | null | undefined;
|
|
@@ -155,6 +146,7 @@ export declare const Config: Schema<{
|
|
|
155
146
|
invalidLinkText?: string | null | undefined;
|
|
156
147
|
parseErrorPrefix?: string | null | undefined;
|
|
157
148
|
parseErrorItemFormat?: string | null | undefined;
|
|
149
|
+
deduplicationTipText?: string | null | undefined;
|
|
158
150
|
}, {
|
|
159
151
|
enable: boolean;
|
|
160
152
|
botName: string;
|
|
@@ -203,21 +195,12 @@ export declare const Config: Schema<{
|
|
|
203
195
|
showMusicCover: boolean;
|
|
204
196
|
showVideoFile: boolean;
|
|
205
197
|
sendLiveMessage: boolean;
|
|
206
|
-
forceDownloadCover: boolean;
|
|
207
|
-
forceDownloadImageNew: boolean;
|
|
208
|
-
forceDownloadAuthorAvatar: boolean;
|
|
209
|
-
forceDownloadVideo: boolean;
|
|
210
198
|
} & {
|
|
211
199
|
showMusicVoice: boolean;
|
|
212
200
|
showMusicVoiceFile: boolean;
|
|
213
|
-
forceDownloadMusicVoice: boolean;
|
|
214
201
|
} & {
|
|
215
202
|
maxDescLength: number;
|
|
216
203
|
maxConcurrent: number;
|
|
217
|
-
downloadConcurrency: number;
|
|
218
|
-
mediaDownloadTimeout: number;
|
|
219
|
-
maxMediaSize: number;
|
|
220
|
-
downloadEngine: "internal" | "aria2" | "downloads";
|
|
221
204
|
} & {
|
|
222
205
|
timeout: number;
|
|
223
206
|
videoSendTimeout: number;
|
|
@@ -246,8 +229,8 @@ export declare const Config: Schema<{
|
|
|
246
229
|
enableForward: boolean;
|
|
247
230
|
} & {
|
|
248
231
|
deduplicationInterval: number;
|
|
232
|
+
enableDeduplication: boolean;
|
|
249
233
|
cacheTTL: number;
|
|
250
|
-
cacheDir: string;
|
|
251
234
|
} & {
|
|
252
235
|
primaryApiUrl: string;
|
|
253
236
|
backupApiUrl: string;
|
|
@@ -328,5 +311,6 @@ export declare const Config: Schema<{
|
|
|
328
311
|
invalidLinkText: string;
|
|
329
312
|
parseErrorPrefix: string;
|
|
330
313
|
parseErrorItemFormat: string;
|
|
314
|
+
deduplicationTipText: string;
|
|
331
315
|
}>;
|
|
332
316
|
export declare function apply(ctx: Context, config: any): void;
|
package/lib/index.js
CHANGED
|
@@ -7,11 +7,6 @@ exports.Config = exports.name = void 0;
|
|
|
7
7
|
exports.apply = apply;
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const fs_1 = require("fs");
|
|
13
|
-
const promises_2 = require("stream/promises");
|
|
14
|
-
const crypto_1 = require("crypto");
|
|
15
10
|
class SimpleLRUCache {
|
|
16
11
|
constructor(max, ttlMs) {
|
|
17
12
|
this.max = max;
|
|
@@ -119,27 +114,14 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
119
114
|
showMusicCover: koishi_1.Schema.boolean().default(true).description('发送音乐封面图片'),
|
|
120
115
|
showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以视频形式发送(关闭则只发送链接)'),
|
|
121
116
|
sendLiveMessage: koishi_1.Schema.boolean().default(true).description('直播作品发送文字消息(不发送视频)'),
|
|
122
|
-
forceDownloadCover: koishi_1.Schema.boolean().default(false).description('强制下载封面'),
|
|
123
|
-
forceDownloadImageNew: koishi_1.Schema.boolean().default(false).description('强制下载图片'),
|
|
124
|
-
forceDownloadAuthorAvatar: koishi_1.Schema.boolean().default(false).description('强制下载作者头像'),
|
|
125
|
-
forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频'),
|
|
126
117
|
}).description('媒体发送'),
|
|
127
118
|
koishi_1.Schema.object({
|
|
128
119
|
showMusicVoice: koishi_1.Schema.boolean().default(false).description('音乐链接以语音形式发送'),
|
|
129
120
|
showMusicVoiceFile: koishi_1.Schema.boolean().default(true).description('音乐链接是否以语音形式发送(关闭则只发送链接)'),
|
|
130
|
-
forceDownloadMusicVoice: koishi_1.Schema.boolean().default(false).description('强制下载音乐语音'),
|
|
131
121
|
}).description('音乐语音(需 silk 和 ffmpeg)'),
|
|
132
122
|
koishi_1.Schema.object({
|
|
133
123
|
maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介长度上限'),
|
|
134
124
|
maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('解析最大并发数'),
|
|
135
|
-
downloadConcurrency: koishi_1.Schema.number().min(1).step(1).default(3).description('下载线程数'),
|
|
136
|
-
mediaDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('统一下载超时 (ms)'),
|
|
137
|
-
maxMediaSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载文件大小 (MB),0 为不限制'),
|
|
138
|
-
downloadEngine: koishi_1.Schema.union([
|
|
139
|
-
koishi_1.Schema.const('internal').description('内置下载'),
|
|
140
|
-
koishi_1.Schema.const('aria2').description('aria2 下载(需 koishi-plugin-aria2-plus)'),
|
|
141
|
-
koishi_1.Schema.const('downloads').description('downloads 服务下载'),
|
|
142
|
-
]).default('internal').description('下载引擎'),
|
|
143
125
|
}).description('性能与限制'),
|
|
144
126
|
koishi_1.Schema.object({
|
|
145
127
|
timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时 (ms)'),
|
|
@@ -171,8 +153,8 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
171
153
|
}).description('发送与重试'),
|
|
172
154
|
koishi_1.Schema.object({
|
|
173
155
|
deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔 (s)'),
|
|
156
|
+
enableDeduplication: koishi_1.Schema.boolean().default(true).description('启用重复解析检测与提示'),
|
|
174
157
|
cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间 (s)'),
|
|
175
|
-
cacheDir: koishi_1.Schema.string().default('./temp_cache').description('统一临时目录'),
|
|
176
158
|
}).description('缓存与临时文件'),
|
|
177
159
|
koishi_1.Schema.object({
|
|
178
160
|
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').hidden(),
|
|
@@ -293,6 +275,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
293
275
|
invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示'),
|
|
294
276
|
parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('错误前缀'),
|
|
295
277
|
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('错误格式'),
|
|
278
|
+
deduplicationTipText: koishi_1.Schema.string().default('链接 ${url} 在最近 ${interval} 秒内已解析过,已跳过。').description('重复解析提示,支持变量 ${url} ${interval}'),
|
|
296
279
|
}).description('界面文本'),
|
|
297
280
|
]);
|
|
298
281
|
const logger = new koishi_1.Logger(exports.name);
|
|
@@ -396,6 +379,22 @@ function linkTypeParser(content, customRules) {
|
|
|
396
379
|
}
|
|
397
380
|
return matches;
|
|
398
381
|
}
|
|
382
|
+
function cleanUrl(url) {
|
|
383
|
+
url = url.replace(/&/g, '&')
|
|
384
|
+
.replace(/"/g, '"')
|
|
385
|
+
.replace(/</g, '<')
|
|
386
|
+
.replace(/>/g, '>')
|
|
387
|
+
.replace(/\\\//g, '/');
|
|
388
|
+
url = url.replace(/^[\s"'<]+/, '');
|
|
389
|
+
url = url.replace(/[\s"'<>\{\}\[\]`,;]+$/, '');
|
|
390
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
391
|
+
if (/^\/\//.test(url))
|
|
392
|
+
url = 'https:' + url;
|
|
393
|
+
else
|
|
394
|
+
return url;
|
|
395
|
+
}
|
|
396
|
+
return url;
|
|
397
|
+
}
|
|
399
398
|
function extractAllUrlsFromMessage(session, customRules) {
|
|
400
399
|
const content = session.content?.trim() || '';
|
|
401
400
|
const matchedLinks = linkTypeParser(content, customRules);
|
|
@@ -426,36 +425,18 @@ function extractAllUrlsFromMessage(session, customRules) {
|
|
|
426
425
|
for (const cardContent of cardsContent) {
|
|
427
426
|
matchedLinks.push(...linkTypeParser(cardContent, customRules));
|
|
428
427
|
}
|
|
429
|
-
const
|
|
430
|
-
const
|
|
428
|
+
const cleanResult = [];
|
|
429
|
+
const seenUrls = new Set();
|
|
431
430
|
for (const link of matchedLinks) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
const cleaned = cleanUrl(link.url);
|
|
432
|
+
if (!cleaned || !/^https?:\/\//i.test(cleaned))
|
|
433
|
+
continue;
|
|
434
|
+
if (!seenUrls.has(cleaned)) {
|
|
435
|
+
seenUrls.add(cleaned);
|
|
436
|
+
cleanResult.push({ ...link, url: cleaned });
|
|
435
437
|
}
|
|
436
438
|
}
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
function cleanUrl(url) {
|
|
440
|
-
try {
|
|
441
|
-
url = url.replace(/&/g, '&');
|
|
442
|
-
const urlObj = new URL(url);
|
|
443
|
-
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
|
|
444
|
-
throw new Error('非法的协议');
|
|
445
|
-
}
|
|
446
|
-
if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
|
|
447
|
-
['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => urlObj.searchParams.delete(p));
|
|
448
|
-
return urlObj.origin + urlObj.pathname;
|
|
449
|
-
}
|
|
450
|
-
if (urlObj.hostname.includes('bilibili.com') || urlObj.hostname.includes('b23.tv')) {
|
|
451
|
-
['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p => urlObj.searchParams.delete(p));
|
|
452
|
-
return urlObj.origin + urlObj.pathname;
|
|
453
|
-
}
|
|
454
|
-
return urlObj.toString();
|
|
455
|
-
}
|
|
456
|
-
catch {
|
|
457
|
-
return url.replace(/&/g, '&').replace(/\?.*/, '');
|
|
458
|
-
}
|
|
439
|
+
return cleanResult;
|
|
459
440
|
}
|
|
460
441
|
function formatDuration(seconds) {
|
|
461
442
|
if (!seconds || seconds <= 0)
|
|
@@ -598,6 +579,9 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
|
|
|
598
579
|
image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
|
|
599
580
|
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
600
581
|
})) : [];
|
|
582
|
+
if (type === 'live' && live_photo.length > 0 && !data.live) {
|
|
583
|
+
type = 'live_photo';
|
|
584
|
+
}
|
|
601
585
|
const musicCoverRaw = mapField('music_cover', () => data.music?.cover || data.music?.albumCover?.url || '');
|
|
602
586
|
const musicUrlRaw = mapField('music_url', () => data.music?.url || data.music?.playURL || '');
|
|
603
587
|
const music = {
|
|
@@ -735,15 +719,18 @@ function apply(ctx, config) {
|
|
|
735
719
|
const imgSig = p.images?.length ? p.images.slice(0, 3).join('|') : (p.live_photo?.slice(0, 3).map(lp => lp.image).join('|') || '');
|
|
736
720
|
return [p.type, p.title, p.author, p.uid, p.video, imgSig].map(v => String(v ?? '')).join('::');
|
|
737
721
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
722
|
+
function getText(key) {
|
|
723
|
+
const defaults = {
|
|
724
|
+
waitingTipText: '正在解析视频,请稍候...',
|
|
725
|
+
unsupportedPlatformText: '不支持该平台链接',
|
|
726
|
+
invalidLinkText: '无效的视频链接',
|
|
727
|
+
parseErrorPrefix: '❌ 解析失败:',
|
|
728
|
+
parseErrorItemFormat: '【${url}】: ${msg}',
|
|
729
|
+
deduplicationTipText: '链接 ${url} 在最近 ${interval} 秒内已解析过,已跳过。',
|
|
730
|
+
};
|
|
731
|
+
return config[key] || defaults[key] || '';
|
|
732
|
+
}
|
|
745
733
|
const proxyConfig = config.proxy || {};
|
|
746
|
-
const cacheDir = config.cacheDir || './temp_cache';
|
|
747
734
|
const customPlatforms = (config.customPlatforms || []).map((p) => ({
|
|
748
735
|
name: p.name,
|
|
749
736
|
apiUrl: p.apiUrl,
|
|
@@ -753,18 +740,6 @@ function apply(ctx, config) {
|
|
|
753
740
|
fieldMapping: parseFieldMapping(p.fieldMapping),
|
|
754
741
|
proxy: p.proxy || null
|
|
755
742
|
}));
|
|
756
|
-
const downloadLimiter = new ConcurrencyLimiter(config.downloadConcurrency || 3);
|
|
757
|
-
const mediaDownloadTimeout = config.mediaDownloadTimeout ?? 120000;
|
|
758
|
-
const maxMediaSize = config.maxMediaSize ?? 0;
|
|
759
|
-
const downloadEngine = config.downloadEngine || 'internal';
|
|
760
|
-
const aria2Service = ctx.get('aria2');
|
|
761
|
-
const downloadsService = ctx.get('downloads');
|
|
762
|
-
if (downloadEngine === 'aria2' && !aria2Service) {
|
|
763
|
-
logger.warn('选择了 aria2 下载引擎,但未检测到 koishi-plugin-aria2-plus 服务,将回退到内置下载');
|
|
764
|
-
}
|
|
765
|
-
if (downloadEngine === 'downloads' && !downloadsService) {
|
|
766
|
-
logger.warn('选择了 downloads 下载引擎,但未检测到 koishi-plugin-downloads 服务,将回退到内置下载');
|
|
767
|
-
}
|
|
768
743
|
function getPlatformConfig(type) {
|
|
769
744
|
if (type.startsWith('custom_')) {
|
|
770
745
|
const name = type.slice(7);
|
|
@@ -831,246 +806,18 @@ function apply(ctx, config) {
|
|
|
831
806
|
return { [customHeaderName]: apiKey };
|
|
832
807
|
return {};
|
|
833
808
|
}
|
|
834
|
-
|
|
835
|
-
async function downloadFile(url, timeout, maxSize, filePrefix, fileExts, onProgress) {
|
|
836
|
-
if (!url)
|
|
837
|
-
throw new Error('链接为空');
|
|
838
|
-
await promises_1.default.mkdir(cacheDir, { recursive: true });
|
|
839
|
-
const ext = fileExts.find(e => {
|
|
840
|
-
const r = extRegexCache[e] || (extRegexCache[e] = new RegExp('\\.' + e + '(\\?|$)', 'i'));
|
|
841
|
-
return r.test(url);
|
|
842
|
-
}) || fileExts[0];
|
|
843
|
-
const fileName = `${filePrefix}_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
|
|
844
|
-
const filePath = path_1.default.resolve(cacheDir, fileName);
|
|
845
|
-
const startTime = Date.now();
|
|
846
|
-
if (downloadEngine === 'downloads' && downloadsService) {
|
|
847
|
-
logger.info(`[下载] 开始 (downloads): ${url}`);
|
|
848
|
-
try {
|
|
849
|
-
const dest = await downloadsService.download(url, path_1.default.join(cacheDir, fileName), {
|
|
850
|
-
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
|
|
851
|
-
timeout
|
|
852
|
-
});
|
|
853
|
-
const stat = await promises_1.default.stat(dest);
|
|
854
|
-
const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
|
855
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
856
|
-
logger.info(`[下载] 完成 (downloads): ${dest} (${sizeMB} MB, 耗时 ${elapsed}s)`);
|
|
857
|
-
if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
|
|
858
|
-
await promises_1.default.unlink(dest).catch(() => { });
|
|
859
|
-
throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
|
|
860
|
-
}
|
|
861
|
-
return dest;
|
|
862
|
-
}
|
|
863
|
-
catch (e) {
|
|
864
|
-
debugLog('ERROR', 'downloads 下载失败,回退内置下载:', e);
|
|
865
|
-
throw e;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
if (downloadEngine === 'aria2' && aria2Service) {
|
|
869
|
-
try {
|
|
870
|
-
logger.info(`[下载] 开始 (aria2): ${url}`);
|
|
871
|
-
const gid = await aria2Service.addUri([url], {
|
|
872
|
-
dir: cacheDir,
|
|
873
|
-
out: fileName,
|
|
874
|
-
split: 4,
|
|
875
|
-
continue: true,
|
|
876
|
-
maxConnectionPerServer: 5,
|
|
877
|
-
timeout: timeout / 1000,
|
|
878
|
-
maxFileNotFound: 5,
|
|
879
|
-
maxTries: 5,
|
|
880
|
-
retryWait: 2,
|
|
881
|
-
header: [`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36`, `Referer: https://www.baidu.com/`]
|
|
882
|
-
});
|
|
883
|
-
let completed = false;
|
|
884
|
-
const ariaStart = Date.now();
|
|
885
|
-
let lastProgressTime = 0;
|
|
886
|
-
while (!completed) {
|
|
887
|
-
if (Date.now() - ariaStart > timeout) {
|
|
888
|
-
await aria2Service.remove(gid).catch(() => { });
|
|
889
|
-
throw new Error('aria2下载超时');
|
|
890
|
-
}
|
|
891
|
-
const status = await aria2Service.tellStatus(gid);
|
|
892
|
-
if (status.status === 'complete') {
|
|
893
|
-
completed = true;
|
|
894
|
-
if (onProgress)
|
|
895
|
-
onProgress(100, '');
|
|
896
|
-
}
|
|
897
|
-
else if (status.status === 'error' || status.status === 'removed') {
|
|
898
|
-
throw new Error('aria2下载失败');
|
|
899
|
-
}
|
|
900
|
-
else {
|
|
901
|
-
if (onProgress && Date.now() - lastProgressTime > 500) {
|
|
902
|
-
lastProgressTime = Date.now();
|
|
903
|
-
const total = parseInt(status.totalLength) || 0;
|
|
904
|
-
const completed = parseInt(status.completedLength) || 0;
|
|
905
|
-
const percent = total > 0 ? Math.round((completed / total) * 100) : -1;
|
|
906
|
-
const speed = formatSpeed(parseInt(status.downloadSpeed) || 0);
|
|
907
|
-
onProgress(percent, speed);
|
|
908
|
-
}
|
|
909
|
-
await delay(1000);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
const stat = await promises_1.default.stat(filePath);
|
|
913
|
-
const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
|
914
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
915
|
-
logger.info(`[下载] 完成 (aria2): ${filePath} (${sizeMB} MB, 耗时 ${elapsed}s)`);
|
|
916
|
-
if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
|
|
917
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
918
|
-
throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
|
|
919
|
-
}
|
|
920
|
-
return filePath;
|
|
921
|
-
}
|
|
922
|
-
catch (e) {
|
|
923
|
-
debugLog('ERROR', `aria2下载失败,回退内置下载: ${getErrorMessage(e)}`);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
const urlObj = new URL(url);
|
|
927
|
-
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
|
|
928
|
-
throw new Error('不支持的下载协议:' + urlObj.protocol);
|
|
929
|
-
}
|
|
930
|
-
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
931
|
-
let response;
|
|
932
|
-
try {
|
|
933
|
-
response = await http({
|
|
934
|
-
method: 'GET',
|
|
935
|
-
url,
|
|
936
|
-
responseType: 'stream',
|
|
937
|
-
timeout,
|
|
938
|
-
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
|
|
939
|
-
maxRedirects: 5,
|
|
940
|
-
validateStatus: (status) => status >= 200 && status < 300,
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
catch (e) {
|
|
944
|
-
writer.destroy();
|
|
945
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
946
|
-
throw new Error(`下载失败: ${getErrorMessage(e)}`);
|
|
947
|
-
}
|
|
948
|
-
const maxSizeBytes = maxSize * 1024 * 1024;
|
|
949
|
-
const contentLength = Number(response.headers['content-length'] || 0);
|
|
950
|
-
if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
|
|
951
|
-
writer.destroy();
|
|
952
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
953
|
-
throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
|
|
954
|
-
}
|
|
955
|
-
const sizeStr = contentLength > 0 ? `${(contentLength / (1024 * 1024)).toFixed(2)}MB` : '未知大小';
|
|
956
|
-
logger.info(`[下载] 开始 (内置): ${url} (${sizeStr})`);
|
|
957
|
-
let downloaded = 0;
|
|
958
|
-
let lastProgressTime = 0;
|
|
959
|
-
response.data.on('data', (chunk) => {
|
|
960
|
-
downloaded += chunk.length;
|
|
961
|
-
if (onProgress && Date.now() - lastProgressTime > 500) {
|
|
962
|
-
lastProgressTime = Date.now();
|
|
963
|
-
if (contentLength > 0) {
|
|
964
|
-
const percent = Math.round((downloaded / contentLength) * 100);
|
|
965
|
-
const mb = (downloaded / (1024 * 1024)).toFixed(2);
|
|
966
|
-
const totalMB = (contentLength / (1024 * 1024)).toFixed(2);
|
|
967
|
-
onProgress(percent, `${mb}/${totalMB}MB`);
|
|
968
|
-
}
|
|
969
|
-
else {
|
|
970
|
-
const mb = (downloaded / (1024 * 1024)).toFixed(2);
|
|
971
|
-
onProgress(-1, `已下载 ${mb} MB`);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
try {
|
|
976
|
-
await (0, promises_2.pipeline)(response.data, writer);
|
|
977
|
-
const stat = await promises_1.default.stat(filePath);
|
|
978
|
-
const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
|
979
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
980
|
-
logger.info(`[下载] 完成 (内置): ${filePath} (${sizeMB} MB, 耗时 ${elapsed}s)`);
|
|
981
|
-
if (onProgress) {
|
|
982
|
-
if (contentLength > 0)
|
|
983
|
-
onProgress(100, `${sizeMB}MB`);
|
|
984
|
-
else
|
|
985
|
-
onProgress(-1, `完成,共 ${sizeMB} MB`);
|
|
986
|
-
}
|
|
987
|
-
return filePath;
|
|
988
|
-
}
|
|
989
|
-
catch (e) {
|
|
990
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
991
|
-
throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
function formatSpeed(bytesPerSec) {
|
|
995
|
-
if (bytesPerSec <= 0)
|
|
996
|
-
return '';
|
|
997
|
-
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
|
998
|
-
let i = 0;
|
|
999
|
-
let speed = bytesPerSec;
|
|
1000
|
-
while (speed >= 1024 && i < units.length - 1) {
|
|
1001
|
-
speed /= 1024;
|
|
1002
|
-
i++;
|
|
1003
|
-
}
|
|
1004
|
-
return `${speed.toFixed(1)}${units[i]}`;
|
|
1005
|
-
}
|
|
1006
|
-
async function sendMedia(session, url, type, forceDownload, showFile) {
|
|
809
|
+
async function sendMedia(session, url, type, showFile) {
|
|
1007
810
|
if (!url)
|
|
1008
811
|
return;
|
|
1009
|
-
|
|
812
|
+
if (!showFile) {
|
|
813
|
+
await sendWithTimeout(session, `${type === 'audio' ? '音乐' : type === 'video' ? '视频' : '图片'}链接:${url}`).catch(() => { });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
1010
816
|
try {
|
|
1011
|
-
|
|
1012
|
-
const extMap = {
|
|
1013
|
-
image: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
|
|
1014
|
-
video: ['mp4'],
|
|
1015
|
-
audio: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']
|
|
1016
|
-
};
|
|
1017
|
-
const prefixMap = { image: 'img', video: 'video', audio: 'music' };
|
|
1018
|
-
const sendFunc = type === 'audio' ? koishi_1.h.audio : type === 'video' ? koishi_1.h.video : koishi_1.h.image;
|
|
1019
|
-
const onProgress = (percent, info) => {
|
|
1020
|
-
if (percent === -1) {
|
|
1021
|
-
logger.info(`[下载] ${info}`);
|
|
1022
|
-
}
|
|
1023
|
-
else {
|
|
1024
|
-
const text = `[下载] ${percent}%` + (info ? ` (${info})` : '');
|
|
1025
|
-
logger.info(text);
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1028
|
-
if (forceDownload) {
|
|
1029
|
-
try {
|
|
1030
|
-
const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type], onProgress);
|
|
1031
|
-
try {
|
|
1032
|
-
await sendWithTimeout(session, sendFunc(`file://${localPath}`));
|
|
1033
|
-
}
|
|
1034
|
-
finally {
|
|
1035
|
-
await promises_1.default.unlink(localPath).catch(() => { });
|
|
1036
|
-
}
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
catch (e) {
|
|
1040
|
-
debugLog('ERROR', `强制下载${type}失败,尝试URL发送:`, getErrorMessage(e));
|
|
1041
|
-
try {
|
|
1042
|
-
await sendWithTimeout(session, sendFunc(url));
|
|
1043
|
-
}
|
|
1044
|
-
catch {
|
|
1045
|
-
await sendLink();
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
if (!showFile) {
|
|
1051
|
-
await sendLink();
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
try {
|
|
1055
|
-
await sendWithTimeout(session, sendFunc(url));
|
|
1056
|
-
}
|
|
1057
|
-
catch {
|
|
1058
|
-
try {
|
|
1059
|
-
const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type], onProgress);
|
|
1060
|
-
try {
|
|
1061
|
-
await sendWithTimeout(session, sendFunc(`file://${localPath}`));
|
|
1062
|
-
}
|
|
1063
|
-
finally {
|
|
1064
|
-
await promises_1.default.unlink(localPath).catch(() => { });
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
catch {
|
|
1068
|
-
await sendLink();
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
817
|
+
await sendWithTimeout(session, type === 'audio' ? koishi_1.h.audio(url) : type === 'video' ? koishi_1.h.video(url) : koishi_1.h.image(url));
|
|
1071
818
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
819
|
+
catch {
|
|
820
|
+
await sendWithTimeout(session, `${type === 'audio' ? '音乐' : type === 'video' ? '视频' : '图片'}链接:${url}`).catch(() => { });
|
|
1074
821
|
}
|
|
1075
822
|
}
|
|
1076
823
|
async function flush(session, matches) {
|
|
@@ -1086,12 +833,13 @@ function apply(ctx, config) {
|
|
|
1086
833
|
debugLog('INFO', `平台 ${match.type} 已禁用,跳过链接: ${match.url}`);
|
|
1087
834
|
return;
|
|
1088
835
|
}
|
|
1089
|
-
if (config.deduplicationInterval > 0) {
|
|
836
|
+
if (config.enableDeduplication !== false && config.deduplicationInterval > 0) {
|
|
1090
837
|
const lastTime = dedupCache.get(match.url);
|
|
1091
838
|
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
1092
839
|
debugLog('INFO', `跳过重复链接: ${match.url}`);
|
|
1093
|
-
const shortUrl = match.url.length >
|
|
1094
|
-
|
|
840
|
+
const shortUrl = match.url.length > 80 ? match.url.slice(0, 80) + '...' : match.url;
|
|
841
|
+
const tip = getText('deduplicationTipText').replace(/\$\{url\}/g, shortUrl).replace(/\$\{interval\}/g, String(config.deduplicationInterval));
|
|
842
|
+
await sendWithTimeout(session, tip).catch(() => { });
|
|
1095
843
|
return;
|
|
1096
844
|
}
|
|
1097
845
|
}
|
|
@@ -1100,7 +848,7 @@ function apply(ctx, config) {
|
|
|
1100
848
|
const fieldMapping = platformConf.fieldMapping;
|
|
1101
849
|
const result = await processSingleUrl(match.url, match.type, fieldMapping, platformConf);
|
|
1102
850
|
if (result.success) {
|
|
1103
|
-
if (config.deduplicationInterval > 0) {
|
|
851
|
+
if (config.enableDeduplication !== false && config.deduplicationInterval > 0) {
|
|
1104
852
|
const fp = contentFingerprint(result.data.parsed);
|
|
1105
853
|
const lastDedup = contentDedupCache.get(fp);
|
|
1106
854
|
if (lastDedup && (Date.now() - lastDedup < config.deduplicationInterval * 1000)) {
|
|
@@ -1113,7 +861,8 @@ function apply(ctx, config) {
|
|
|
1113
861
|
items.push(result.data);
|
|
1114
862
|
}
|
|
1115
863
|
else {
|
|
1116
|
-
const
|
|
864
|
+
const displayUrl = match.url.length > 80 ? match.url.slice(0, 80) + '...' : match.url;
|
|
865
|
+
const item = getText('parseErrorItemFormat').replace(/\$\{url\}/g, displayUrl).replace(/\$\{msg\}/g, result.msg);
|
|
1117
866
|
errors.push(item);
|
|
1118
867
|
}
|
|
1119
868
|
}
|
|
@@ -1123,7 +872,7 @@ function apply(ctx, config) {
|
|
|
1123
872
|
});
|
|
1124
873
|
await Promise.all(promises);
|
|
1125
874
|
if (errors.length)
|
|
1126
|
-
await sendWithTimeout(session, `${
|
|
875
|
+
await sendWithTimeout(session, `${getText('parseErrorPrefix')}\n${errors.join('\n')}`);
|
|
1127
876
|
if (!items.length)
|
|
1128
877
|
return;
|
|
1129
878
|
const enableForward = config.enableForward && (session.platform === 'onebot' || session.platform === 'satori');
|
|
@@ -1151,26 +900,33 @@ function apply(ctx, config) {
|
|
|
1151
900
|
if (config.showMusicCover && p.music.cover) {
|
|
1152
901
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.music.cover), botName));
|
|
1153
902
|
}
|
|
1154
|
-
if (p.type === '
|
|
903
|
+
if (p.type === 'live_photo' && p.live_photo?.length) {
|
|
904
|
+
for (const lp of p.live_photo) {
|
|
905
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(lp.image), botName));
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
else if (p.type === 'image' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
1155
909
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
1156
910
|
for (const imgUrl of imageUrls) {
|
|
1157
911
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
|
|
1158
912
|
}
|
|
1159
913
|
}
|
|
1160
|
-
if (p.video && p.type !== 'live') {
|
|
914
|
+
if (p.video && p.type !== 'live' && p.type !== 'live_photo') {
|
|
1161
915
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
|
|
1162
916
|
}
|
|
1163
917
|
if (config.showMusicVoice && p.music.url) {
|
|
1164
918
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.audio(p.music.url), botName));
|
|
1165
919
|
}
|
|
1166
920
|
}
|
|
1167
|
-
|
|
921
|
+
const MAX_NODES = 50;
|
|
922
|
+
for (let i = 0; i < forwardMessages.length; i += MAX_NODES) {
|
|
923
|
+
const batch = forwardMessages.slice(i, i + MAX_NODES);
|
|
1168
924
|
try {
|
|
1169
|
-
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true },
|
|
925
|
+
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, batch), config.retryTimes);
|
|
1170
926
|
}
|
|
1171
927
|
catch (err) {
|
|
1172
928
|
debugLog('ERROR', '合并转发失败,降级逐条发送:', err);
|
|
1173
|
-
for (const node of
|
|
929
|
+
for (const node of batch) {
|
|
1174
930
|
await sendWithTimeout(session, node.data.content).catch(() => { });
|
|
1175
931
|
await delay(300);
|
|
1176
932
|
}
|
|
@@ -1189,28 +945,30 @@ function apply(ctx, config) {
|
|
|
1189
945
|
await delay(300);
|
|
1190
946
|
}
|
|
1191
947
|
if (config.showAuthorAvatar && p.avatar) {
|
|
1192
|
-
await sendMedia(session, p.avatar, 'image', config.
|
|
948
|
+
await sendMedia(session, p.avatar, 'image', config.showAuthorAvatarFile).catch(() => { });
|
|
1193
949
|
await delay(300);
|
|
1194
950
|
}
|
|
1195
951
|
if (p.cover && config.showCoverImage && p.type !== 'live_photo' && p.type !== 'image' && p.type !== 'live') {
|
|
1196
952
|
if (config.showCoverText)
|
|
1197
953
|
await sendWithTimeout(session, config.coverText || '封面:');
|
|
1198
|
-
await sendMedia(session, p.cover, 'image', config.
|
|
954
|
+
await sendMedia(session, p.cover, 'image', config.showCoverFile).catch(() => { });
|
|
1199
955
|
await delay(300);
|
|
1200
956
|
}
|
|
1201
957
|
if (config.showMusicCover && p.music.cover) {
|
|
1202
|
-
await sendMedia(session, p.music.cover, 'image',
|
|
958
|
+
await sendMedia(session, p.music.cover, 'image', true).catch(() => { });
|
|
1203
959
|
await delay(300);
|
|
1204
960
|
}
|
|
1205
|
-
if (p.
|
|
1206
|
-
|
|
1207
|
-
|
|
961
|
+
if (p.type === 'live_photo' && p.live_photo?.length) {
|
|
962
|
+
for (const lp of p.live_photo) {
|
|
963
|
+
await sendMedia(session, lp.image, 'image', config.showImageFileNew).catch(() => { });
|
|
964
|
+
await delay(500);
|
|
965
|
+
}
|
|
1208
966
|
}
|
|
1209
|
-
if (p.type === 'image' ||
|
|
967
|
+
else if (p.type === 'image' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
1210
968
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
1211
969
|
for (let i = 0; i < imageUrls.length; i++) {
|
|
1212
970
|
logger.info(`[发送] 图片 ${i + 1}/${imageUrls.length}`);
|
|
1213
|
-
await sendMedia(session, imageUrls[i], 'image', config.
|
|
971
|
+
await sendMedia(session, imageUrls[i], 'image', config.showImageFileNew).catch(() => { });
|
|
1214
972
|
await delay(1000);
|
|
1215
973
|
}
|
|
1216
974
|
}
|
|
@@ -1218,7 +976,7 @@ function apply(ctx, config) {
|
|
|
1218
976
|
await sendWithTimeout(session, '直播进行中,无法发送视频流。').catch(() => { });
|
|
1219
977
|
}
|
|
1220
978
|
if (config.showMusicVoice && p.music.url) {
|
|
1221
|
-
await sendMedia(session, p.music.url, 'audio', config.
|
|
979
|
+
await sendMedia(session, p.music.url, 'audio', config.showMusicVoiceFile).catch(() => { });
|
|
1222
980
|
await delay(300);
|
|
1223
981
|
}
|
|
1224
982
|
}
|
|
@@ -1326,7 +1084,7 @@ function apply(ctx, config) {
|
|
|
1326
1084
|
debugLog('ERROR', `解析失败: ${cleanedUrl}`, getErrorMessage(error));
|
|
1327
1085
|
return { success: false, msg: getErrorMessage(error) };
|
|
1328
1086
|
}
|
|
1329
|
-
return { success: false, msg:
|
|
1087
|
+
return { success: false, msg: getText('unsupportedPlatformText') };
|
|
1330
1088
|
}
|
|
1331
1089
|
async function processSingleUrl(url, type, fieldMapping, platformConf) {
|
|
1332
1090
|
const result = await parseUrl(url, type, fieldMapping, platformConf);
|
|
@@ -1398,7 +1156,7 @@ function apply(ctx, config) {
|
|
|
1398
1156
|
debugLog('INFO', `检测到 ${matches.length} 个链接`);
|
|
1399
1157
|
if (config.showWaitingTip) {
|
|
1400
1158
|
try {
|
|
1401
|
-
await sendWithTimeout(session,
|
|
1159
|
+
await sendWithTimeout(session, getText('waitingTipText'));
|
|
1402
1160
|
}
|
|
1403
1161
|
catch (e) {
|
|
1404
1162
|
debugLog('WARN', '等待提示发送失败:', e);
|
|
@@ -1408,66 +1166,26 @@ function apply(ctx, config) {
|
|
|
1408
1166
|
});
|
|
1409
1167
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
1410
1168
|
if (!url) {
|
|
1411
|
-
await sendWithTimeout(session,
|
|
1169
|
+
await sendWithTimeout(session, getText('invalidLinkText'));
|
|
1412
1170
|
return;
|
|
1413
1171
|
}
|
|
1414
1172
|
const matches = linkTypeParser(url, customRules);
|
|
1415
1173
|
if (!matches.length) {
|
|
1416
|
-
await sendWithTimeout(session,
|
|
1174
|
+
await sendWithTimeout(session, getText('invalidLinkText'));
|
|
1417
1175
|
return;
|
|
1418
1176
|
}
|
|
1419
1177
|
if (config.showWaitingTip) {
|
|
1420
1178
|
try {
|
|
1421
|
-
await sendWithTimeout(session,
|
|
1179
|
+
await sendWithTimeout(session, getText('waitingTipText'));
|
|
1422
1180
|
}
|
|
1423
1181
|
catch { }
|
|
1424
1182
|
}
|
|
1425
1183
|
await flush(session, matches);
|
|
1426
1184
|
});
|
|
1427
|
-
const tempCleanupInterval = setInterval(async () => {
|
|
1428
|
-
try {
|
|
1429
|
-
const files = await promises_1.default.readdir(cacheDir);
|
|
1430
|
-
const now = Date.now();
|
|
1431
|
-
for (const file of files) {
|
|
1432
|
-
if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
|
|
1433
|
-
(file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
|
|
1434
|
-
(file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
|
|
1435
|
-
const filePath = path_1.default.join(cacheDir, file);
|
|
1436
|
-
const stats = await promises_1.default.stat(filePath);
|
|
1437
|
-
if (now - stats.mtimeMs > 3600000) {
|
|
1438
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
catch (e) {
|
|
1444
|
-
if (e?.code !== 'ENOENT')
|
|
1445
|
-
debugLog('WARN', '清理临时文件失败:', e);
|
|
1446
|
-
}
|
|
1447
|
-
}, 3600000);
|
|
1448
1185
|
ctx.on('dispose', () => {
|
|
1449
|
-
clearInterval(tempCleanupInterval);
|
|
1450
1186
|
urlCacheLocal.clear();
|
|
1451
1187
|
dedupCache.clear();
|
|
1452
1188
|
debugLog('INFO', '插件已卸载');
|
|
1453
1189
|
});
|
|
1454
|
-
if (!process.listenerCount('beforeExit')) {
|
|
1455
|
-
process.once('beforeExit', async () => {
|
|
1456
|
-
try {
|
|
1457
|
-
const files = await promises_1.default.readdir(cacheDir);
|
|
1458
|
-
for (const file of files) {
|
|
1459
|
-
if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
|
|
1460
|
-
(file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
|
|
1461
|
-
(file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
|
|
1462
|
-
await promises_1.default.unlink(path_1.default.join(cacheDir, file)).catch(() => { });
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
catch (e) {
|
|
1467
|
-
if (e?.code !== 'ENOENT')
|
|
1468
|
-
debugLog('WARN', '退出清理临时文件失败:', e);
|
|
1469
|
-
}
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
1190
|
debugLog('INFO', '插件初始化完成');
|
|
1473
1191
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-video-parser-all",
|
|
3
|
-
"description": "Koishi 全平台视频/图集解析插件,支持抖音/快手/B
|
|
4
|
-
"version": "1.5.
|
|
3
|
+
"description": "Koishi 全平台视频/图集解析插件,支持抖音/快手/B站/微博/小红书/YouTube/TikTok等20+平台",
|
|
4
|
+
"version": "1.5.7",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"lib"
|
|
9
|
-
],
|
|
7
|
+
"files": ["lib"],
|
|
10
8
|
"license": "MIT",
|
|
11
9
|
"scripts": {
|
|
12
10
|
"build": "tsc",
|
|
@@ -87,35 +85,14 @@
|
|
|
87
85
|
"最右"
|
|
88
86
|
],
|
|
89
87
|
"devDependencies": {
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"rimraf": "^5.0.5",
|
|
93
|
-
"typescript": "^5.3.3"
|
|
88
|
+
"typescript": "^5.3.3",
|
|
89
|
+
"rimraf": "^5.0.5"
|
|
94
90
|
},
|
|
95
91
|
"dependencies": {
|
|
96
92
|
"axios": "^1.16.1"
|
|
97
93
|
},
|
|
98
94
|
"peerDependencies": {
|
|
99
|
-
"
|
|
100
|
-
"koishi": "^4.18.7",
|
|
101
|
-
"koishi-plugin-downloads": "^1.0.0",
|
|
102
|
-
"koishi-plugin-silk": "^1.0.0",
|
|
103
|
-
"koishi-plugin-ffmpeg": "^1.0.0",
|
|
104
|
-
"koishi-plugin-aria2-plus": "^1.0.0"
|
|
105
|
-
},
|
|
106
|
-
"peerDependenciesMeta": {
|
|
107
|
-
"koishi-plugin-downloads": {
|
|
108
|
-
"optional": true
|
|
109
|
-
},
|
|
110
|
-
"koishi-plugin-silk": {
|
|
111
|
-
"optional": true
|
|
112
|
-
},
|
|
113
|
-
"koishi-plugin-ffmpeg": {
|
|
114
|
-
"optional": true
|
|
115
|
-
},
|
|
116
|
-
"koishi-plugin-aria2-plus": {
|
|
117
|
-
"optional": true
|
|
118
|
-
}
|
|
95
|
+
"koishi": "^4.18.7"
|
|
119
96
|
},
|
|
120
97
|
"repository": {
|
|
121
98
|
"type": "git",
|
package/readme.md
CHANGED
|
@@ -32,7 +32,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
32
32
|
### 消息格式 (Message Format)
|
|
33
33
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
34
34
|
|----------------|-------------|-------------------|---------------------|
|
|
35
|
-
| `unifiedMessageFormat` | string | 见预设 (See preset) |
|
|
35
|
+
| `unifiedMessageFormat` | string | 见预设 (See preset) | 文字格式,支持变量:${标题} ${作者} ${简介} ${视频时长} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${发布时间} ${图片数量} ${作者ID} ${音乐标题} ${音乐作者},空行自动隐藏 (Text format, supports variables: ${标题} ${作者} ${简介} ${视频时长} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${发布时间} ${图片数量} ${作者ID} ${音乐标题} ${音乐作者}, auto-hide empty lines) |
|
|
36
36
|
|
|
37
37
|
### 媒体发送 (Media Sending)
|
|
38
38
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
@@ -50,27 +50,18 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
50
50
|
| `showMusicCover` | boolean | true | 发送音乐封面图片 (Send music cover image) |
|
|
51
51
|
| `showVideoFile` | boolean | true | 视频是否以视频形式发送(关闭则只发送链接)(Send video as file, otherwise link only) |
|
|
52
52
|
| `sendLiveMessage` | boolean | true | 直播作品发送文字消息(不发送视频)(Send text message for live streams, no video) |
|
|
53
|
-
| `forceDownloadCover` | boolean | false | 强制下载封面 (Force download cover) |
|
|
54
|
-
| `forceDownloadImageNew` | boolean | false | 强制下载图片 (Force download images) |
|
|
55
|
-
| `forceDownloadAuthorAvatar` | boolean | false | 强制下载作者头像 (Force download author avatar) |
|
|
56
|
-
| `forceDownloadVideo` | boolean | false | 强制下载视频 (Force download video) |
|
|
57
53
|
|
|
58
|
-
###
|
|
54
|
+
### 音乐语音 (Music Voice)
|
|
59
55
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
60
56
|
|----------------|-------------|-------------------|---------------------|
|
|
61
57
|
| `showMusicVoice` | boolean | false | 音乐链接以语音发送 (Send music as voice) |
|
|
62
58
|
| `showMusicVoiceFile` | boolean | true | 音乐链接是否以语音形式发送(关闭则只发送链接)(Send as voice file, otherwise link only) |
|
|
63
|
-
| `forceDownloadMusicVoice` | boolean | false | 强制下载音乐语音 (Force download music voice) |
|
|
64
59
|
|
|
65
60
|
### 性能与限制 (Performance & Limits)
|
|
66
61
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
67
62
|
|----------------|-------------|-------------------|---------------------|
|
|
68
63
|
| `maxDescLength` | number | 200 | 简介长度上限 (Max description length) |
|
|
69
64
|
| `maxConcurrent` | number | 3 | 解析最大并发数 (Max concurrent parsing) |
|
|
70
|
-
| `downloadConcurrency` | number | 3 | 下载线程数 (Download concurrency) |
|
|
71
|
-
| `mediaDownloadTimeout` | number | 120000 | 统一下载超时 (ms) (Download timeout) |
|
|
72
|
-
| `maxMediaSize` | number | 0 | 最大下载文件大小 (MB),0 为不限制 (Max file size, 0 = unlimited) |
|
|
73
|
-
| `downloadEngine` | string | internal | 下载引擎(internal / aria2 / downloads)(Download engine) |
|
|
74
65
|
|
|
75
66
|
### 网络与请求 (Network & Request)
|
|
76
67
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
@@ -89,12 +80,12 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
89
80
|
| `retryInterval` | number | 1000 | 重试间隔 (ms) (Retry interval) |
|
|
90
81
|
| `enableForward` | boolean | false | 合并转发(OneBot/Satori)(Enable forward message) |
|
|
91
82
|
|
|
92
|
-
###
|
|
83
|
+
### 缓存与去重 (Cache & Deduplication)
|
|
93
84
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
94
85
|
|----------------|-------------|-------------------|---------------------|
|
|
86
|
+
| `enableDeduplication` | boolean | true | 启用重复解析检测与提示 (Enable duplicate detection) |
|
|
95
87
|
| `deduplicationInterval` | number | 180 | 去重间隔 (s) (Deduplication interval) |
|
|
96
88
|
| `cacheTTL` | number | 600 | 缓存时间 (s) (Cache TTL) |
|
|
97
|
-
| `cacheDir` | string | ./temp_cache | 统一临时目录 (Temp directory) |
|
|
98
89
|
|
|
99
90
|
### API 与平台 (API & Platforms)
|
|
100
91
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
@@ -112,10 +103,12 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
112
103
|
| `invalidLinkText` | string | 无效链接 | 无效链接提示 (Invalid link tip) |
|
|
113
104
|
| `parseErrorPrefix` | string | ❌ 解析失败: | 错误前缀 (Error prefix) |
|
|
114
105
|
| `parseErrorItemFormat` | string | ... | 错误格式 (Error format) |
|
|
106
|
+
| `deduplicationTipText` | string | 链接 ${url} 在最近 ${interval} 秒内已解析过,已跳过。 | 重复解析提示 (Duplication tip) |
|
|
115
107
|
|
|
116
108
|
## 支持的变量 (Supported Variables)
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
|
|
110
|
+
> 在 `unifiedMessageFormat` 中可使用以下变量,空行自动隐藏。
|
|
111
|
+
> The following variables can be used in `unifiedMessageFormat`, empty lines are auto-hidden.
|
|
119
112
|
|
|
120
113
|
| 变量 (Variable) | 说明 (Description) |
|
|
121
114
|
|----------------|--------------------|
|
|
@@ -134,21 +127,6 @@ The following variables can be used in `unifiedMessageFormat`, empty lines are a
|
|
|
134
127
|
| `${音乐标题}` | 音乐标题 (Music title) |
|
|
135
128
|
| `${音乐作者}` | 音乐作者 (Music author) |
|
|
136
129
|
|
|
137
|
-
## 依赖说明 (Dependencies)
|
|
138
|
-
### 音乐语音(可选)(Music Voice - Optional)
|
|
139
|
-
若启用 `showMusicVoice`,请安装:
|
|
140
|
-
If you enable `showMusicVoice`, please install:
|
|
141
|
-
- `koishi-plugin-silk`:silk 编解码 (Silk codec)
|
|
142
|
-
- `koishi-plugin-ffmpeg`:音频重采样 (Audio resampling)
|
|
143
|
-
|
|
144
|
-
### aria2 下载引擎(可选)(aria2 Download Engine - Optional)
|
|
145
|
-
若启用 `downloadEngine: 'aria2'`,请安装可选依赖 `koishi-plugin-aria2-plus` 并配置该插件连接 aria2 服务。插件启动时会自动检测该服务,未安装或不可用时将降级为内置下载,不影响正常使用。
|
|
146
|
-
If you use `downloadEngine: 'aria2'`, please install the optional dependency `koishi-plugin-aria2-plus` and configure it to connect to an aria2 service. The plugin will auto-detect it; if not available, it will fall back to the built-in downloader without affecting normal usage.
|
|
147
|
-
|
|
148
|
-
### downloads 服务(可选)(Downloads Service - Optional)
|
|
149
|
-
若启用 `downloadEngine: 'downloads'`,请安装可选依赖 `koishi-plugin-downloads`,失败时回退到内置下载。
|
|
150
|
-
If you use `downloadEngine: 'downloads'`, please install the optional dependency `koishi-plugin-downloads`. It will fall back to the built-in downloader on failure.
|
|
151
|
-
|
|
152
130
|
## 支持的平台 (Supported Platforms)
|
|
153
131
|
|
|
154
132
|
> 以下为插件内置链接匹配规则,可根据用户发送的链接自动识别。所有匹配规则同时支持 HTTP 和 HTTPS 协议,并兼容多级路径(如短链后带 `/` 子路径)。
|