koishi-plugin-video-parser-all 1.2.5 → 1.2.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 +12 -2
- package/lib/index.js +151 -229
- package/package.json +1 -1
- package/readme.md +6 -4
package/lib/index.d.ts
CHANGED
|
@@ -48,10 +48,15 @@ export declare const Config: Schema<{
|
|
|
48
48
|
twitter?: boolean | null | undefined;
|
|
49
49
|
instagram?: boolean | null | undefined;
|
|
50
50
|
doubao?: boolean | null | undefined;
|
|
51
|
+
oasis?: boolean | null | undefined;
|
|
52
|
+
wechat_channel?: boolean | null | undefined;
|
|
51
53
|
} & import("cosmokit").Dict) | null | undefined;
|
|
52
54
|
customApis?: ({
|
|
53
|
-
platform?: "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | null | undefined;
|
|
55
|
+
platform?: "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel" | null | undefined;
|
|
54
56
|
apiUrl?: string | null | undefined;
|
|
57
|
+
apiKey?: string | null | undefined;
|
|
58
|
+
authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
|
|
59
|
+
customHeaderName?: string | null | undefined;
|
|
55
60
|
} & import("cosmokit").Dict)[] | null | undefined;
|
|
56
61
|
} & {
|
|
57
62
|
waitingTipText?: string | null | undefined;
|
|
@@ -107,10 +112,15 @@ export declare const Config: Schema<{
|
|
|
107
112
|
twitter: Schema<boolean, boolean>;
|
|
108
113
|
instagram: Schema<boolean, boolean>;
|
|
109
114
|
doubao: Schema<boolean, boolean>;
|
|
115
|
+
oasis: Schema<boolean, boolean>;
|
|
116
|
+
wechat_channel: Schema<boolean, boolean>;
|
|
110
117
|
}>;
|
|
111
118
|
customApis: Schemastery.ObjectT<{
|
|
112
|
-
platform: Schema<"bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao", "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao">;
|
|
119
|
+
platform: Schema<"bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel", "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel">;
|
|
113
120
|
apiUrl: Schema<string, string>;
|
|
121
|
+
apiKey: Schema<string, string>;
|
|
122
|
+
authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
|
|
123
|
+
customHeaderName: Schema<string, string>;
|
|
114
124
|
}>[];
|
|
115
125
|
} & {
|
|
116
126
|
waitingTipText: string;
|
package/lib/index.js
CHANGED
|
@@ -38,7 +38,9 @@ class SimpleLRUCache {
|
|
|
38
38
|
}
|
|
39
39
|
this.map.set(key, { value, expireAt: Date.now() + this.ttlMs });
|
|
40
40
|
}
|
|
41
|
-
clear() {
|
|
41
|
+
clear() {
|
|
42
|
+
this.map.clear();
|
|
43
|
+
}
|
|
42
44
|
}
|
|
43
45
|
exports.name = 'video-parser-all';
|
|
44
46
|
exports.Config = koishi_1.Schema.intersect([
|
|
@@ -78,25 +80,27 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
78
80
|
}).description('去重设置'),
|
|
79
81
|
koishi_1.Schema.object({
|
|
80
82
|
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
|
|
81
|
-
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API
|
|
83
|
+
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址'),
|
|
82
84
|
platformDedicatedFirst: koishi_1.Schema.object({
|
|
83
|
-
bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
|
|
84
|
-
douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
|
|
85
|
-
kuaishou: koishi_1.Schema.boolean().default(false).description('快手'),
|
|
86
|
-
xiaohongshu: koishi_1.Schema.boolean().default(false).description('小红书'),
|
|
87
|
-
weibo: koishi_1.Schema.boolean().default(false).description('微博'),
|
|
88
|
-
xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
|
|
89
|
-
youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
|
|
90
|
-
tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
|
|
91
|
-
acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
|
|
92
|
-
zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
|
|
93
|
-
weishi: koishi_1.Schema.boolean().default(false).description('微视'),
|
|
94
|
-
huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
|
|
95
|
-
haokan: koishi_1.Schema.boolean().default(false).description('好看视频'),
|
|
96
|
-
meipai: koishi_1.Schema.boolean().default(false).description('美拍'),
|
|
97
|
-
twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X'),
|
|
98
|
-
instagram: koishi_1.Schema.boolean().default(false).description('Instagram'),
|
|
99
|
-
doubao: koishi_1.Schema.boolean().default(false).description('豆包'),
|
|
85
|
+
bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩 - 优先使用专属 API'),
|
|
86
|
+
douyin: koishi_1.Schema.boolean().default(false).description('抖音 - 优先使用专属 API'),
|
|
87
|
+
kuaishou: koishi_1.Schema.boolean().default(false).description('快手 - 优先使用专属 API'),
|
|
88
|
+
xiaohongshu: koishi_1.Schema.boolean().default(false).description('小红书 - 优先使用专属 API'),
|
|
89
|
+
weibo: koishi_1.Schema.boolean().default(false).description('微博 - 优先使用专属 API'),
|
|
90
|
+
xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频 - 优先使用专属 API'),
|
|
91
|
+
youtube: koishi_1.Schema.boolean().default(false).description('YouTube - 优先使用专属 API'),
|
|
92
|
+
tiktok: koishi_1.Schema.boolean().default(false).description('TikTok - 优先使用专属 API'),
|
|
93
|
+
acfun: koishi_1.Schema.boolean().default(false).description('AcFun - 优先使用专属 API'),
|
|
94
|
+
zhihu: koishi_1.Schema.boolean().default(false).description('知乎 - 优先使用专属 API'),
|
|
95
|
+
weishi: koishi_1.Schema.boolean().default(false).description('微视 - 优先使用专属 API'),
|
|
96
|
+
huya: koishi_1.Schema.boolean().default(false).description('虎牙 - 优先使用专属 API'),
|
|
97
|
+
haokan: koishi_1.Schema.boolean().default(false).description('好看视频 - 优先使用专属 API'),
|
|
98
|
+
meipai: koishi_1.Schema.boolean().default(false).description('美拍 - 优先使用专属 API'),
|
|
99
|
+
twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X - 优先使用专属 API'),
|
|
100
|
+
instagram: koishi_1.Schema.boolean().default(false).description('Instagram - 优先使用专属 API'),
|
|
101
|
+
doubao: koishi_1.Schema.boolean().default(false).description('豆包 - 优先使用专属 API'),
|
|
102
|
+
oasis: koishi_1.Schema.boolean().default(false).description('绿洲 - 优先使用专属 API'),
|
|
103
|
+
wechat_channel: koishi_1.Schema.boolean().default(false).description('视频号 - 优先使用专属 API'),
|
|
100
104
|
}).description('各平台独立开关:是否优先使用专属 API'),
|
|
101
105
|
customApis: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
102
106
|
platform: koishi_1.Schema.union([
|
|
@@ -117,16 +121,25 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
117
121
|
koishi_1.Schema.const('twitter').description('Twitter/X'),
|
|
118
122
|
koishi_1.Schema.const('instagram').description('Instagram'),
|
|
119
123
|
koishi_1.Schema.const('doubao').description('豆包'),
|
|
124
|
+
koishi_1.Schema.const('oasis').description('绿洲'),
|
|
125
|
+
koishi_1.Schema.const('wechat_channel').description('视频号'),
|
|
120
126
|
]).description('选择平台'),
|
|
121
127
|
apiUrl: koishi_1.Schema.string().description('API 地址'),
|
|
128
|
+
apiKey: koishi_1.Schema.string().description('API Key(可选)').default(''),
|
|
129
|
+
authHeaderType: koishi_1.Schema.union([
|
|
130
|
+
koishi_1.Schema.const('Bearer').description('Bearer Token'),
|
|
131
|
+
koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
|
|
132
|
+
koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
|
|
133
|
+
]).default('Bearer').description('认证头类型'),
|
|
134
|
+
customHeaderName: koishi_1.Schema.string().description('自定义 Header 名称(仅当选择 Custom 时有效)').default('X-API-Key'),
|
|
122
135
|
})).default([]).description('自定义平台专属 API 地址,留空则使用内置默认专属 API'),
|
|
123
136
|
}).description('API 选择设置'),
|
|
124
137
|
koishi_1.Schema.object({
|
|
125
|
-
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('
|
|
126
|
-
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('
|
|
138
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示文字'),
|
|
139
|
+
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示文字'),
|
|
127
140
|
invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示(parse 指令)'),
|
|
128
141
|
parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('解析失败消息前缀'),
|
|
129
|
-
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('
|
|
142
|
+
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败的展示格式,可用 ${url}(链接)和 ${msg}(错误信息)'),
|
|
130
143
|
}).description('界面文字设置'),
|
|
131
144
|
]);
|
|
132
145
|
const logger = new koishi_1.Logger(exports.name);
|
|
@@ -134,19 +147,7 @@ let debugEnabled = false;
|
|
|
134
147
|
function debugLog(level, ...args) {
|
|
135
148
|
if (!debugEnabled)
|
|
136
149
|
return;
|
|
137
|
-
|
|
138
|
-
const message = `[${timestamp}] [${level}] ${args.map(a => {
|
|
139
|
-
if (typeof a === 'object') {
|
|
140
|
-
try {
|
|
141
|
-
return JSON.stringify(a, null, 2);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return String(a);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return String(a);
|
|
148
|
-
}).join(' ')}`;
|
|
149
|
-
logger.info(message);
|
|
150
|
+
logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
|
|
150
151
|
}
|
|
151
152
|
const urlCache = new SimpleLRUCache(500, 10 * 60 * 1000);
|
|
152
153
|
const LINK_RULES = [
|
|
@@ -176,6 +177,8 @@ const LINK_RULES = [
|
|
|
176
177
|
{ pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
|
|
177
178
|
{ pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
|
|
178
179
|
{ pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
|
|
180
|
+
{ pattern: /https?:\/\/(?:www\.)?oasis\.weibo\.com\/v\/[0-9a-zA-Z_-]+/gi, type: 'oasis' },
|
|
181
|
+
{ pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
179
182
|
];
|
|
180
183
|
function linkTypeParser(content) {
|
|
181
184
|
content = content.replace(/\\\//g, '/');
|
|
@@ -200,9 +203,8 @@ function extractAllUrlsFromMessage(session) {
|
|
|
200
203
|
const cardsContent = [];
|
|
201
204
|
if (session.elements) {
|
|
202
205
|
for (const elem of session.elements) {
|
|
203
|
-
if (elem.type === 'xml' && elem.data)
|
|
206
|
+
if (elem.type === 'xml' && elem.data)
|
|
204
207
|
cardsContent.push(elem.data);
|
|
205
|
-
}
|
|
206
208
|
else if (elem.type === 'json' && elem.data) {
|
|
207
209
|
try {
|
|
208
210
|
const json = JSON.parse(elem.data);
|
|
@@ -210,9 +212,8 @@ function extractAllUrlsFromMessage(session) {
|
|
|
210
212
|
if (!obj || typeof obj !== 'object')
|
|
211
213
|
return;
|
|
212
214
|
for (const val of Object.values(obj)) {
|
|
213
|
-
if (typeof val === 'string')
|
|
215
|
+
if (typeof val === 'string')
|
|
214
216
|
cardsContent.push(val);
|
|
215
|
-
}
|
|
216
217
|
else if (typeof val === 'object')
|
|
217
218
|
extract(val);
|
|
218
219
|
}
|
|
@@ -224,8 +225,7 @@ function extractAllUrlsFromMessage(session) {
|
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
for (const cardContent of cardsContent) {
|
|
227
|
-
|
|
228
|
-
matchedLinks.push(...cardLinks);
|
|
228
|
+
matchedLinks.push(...linkTypeParser(cardContent));
|
|
229
229
|
}
|
|
230
230
|
const seen = new Set();
|
|
231
231
|
const result = [];
|
|
@@ -241,25 +241,19 @@ function cleanUrl(url) {
|
|
|
241
241
|
try {
|
|
242
242
|
url = url.replace(/&/g, '&');
|
|
243
243
|
const urlObj = new URL(url);
|
|
244
|
-
if (urlObj.protocol === 'http:')
|
|
244
|
+
if (urlObj.protocol === 'http:')
|
|
245
245
|
urlObj.protocol = 'https:';
|
|
246
|
-
}
|
|
247
246
|
if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
|
|
248
|
-
['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p =>
|
|
249
|
-
urlObj.searchParams.delete(p);
|
|
250
|
-
});
|
|
247
|
+
['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => urlObj.searchParams.delete(p));
|
|
251
248
|
return urlObj.origin + urlObj.pathname;
|
|
252
249
|
}
|
|
253
250
|
if (urlObj.hostname.includes('bilibili.com') || urlObj.hostname.includes('b23.tv')) {
|
|
254
|
-
['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p =>
|
|
255
|
-
urlObj.searchParams.delete(p);
|
|
256
|
-
});
|
|
251
|
+
['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p => urlObj.searchParams.delete(p));
|
|
257
252
|
return urlObj.origin + urlObj.pathname;
|
|
258
253
|
}
|
|
259
254
|
return urlObj.toString();
|
|
260
255
|
}
|
|
261
|
-
catch
|
|
262
|
-
debugLog('WARN', '清理URL失败:', e, '原始URL:', url);
|
|
256
|
+
catch {
|
|
263
257
|
return url.replace(/&/g, '&').replace(/\?.*/, '');
|
|
264
258
|
}
|
|
265
259
|
}
|
|
@@ -277,23 +271,24 @@ function formatPublishTime(ms) {
|
|
|
277
271
|
if (!ms)
|
|
278
272
|
return '';
|
|
279
273
|
const d = new Date(ms);
|
|
280
|
-
const y = d.getFullYear()
|
|
274
|
+
const y = d.getFullYear();
|
|
275
|
+
const mo = (d.getMonth() + 1).toString().padStart(2, '0');
|
|
276
|
+
const day = d.getDate().toString().padStart(2, '0');
|
|
277
|
+
const H = d.getHours().toString().padStart(2, '0');
|
|
278
|
+
const i = d.getMinutes().toString().padStart(2, '0');
|
|
281
279
|
return `${y}年${mo}月${day}日 ${H}:${i}`;
|
|
282
280
|
}
|
|
283
281
|
function pickBestQuality(videoBackup) {
|
|
284
282
|
if (!Array.isArray(videoBackup))
|
|
285
283
|
return [];
|
|
286
|
-
return videoBackup
|
|
287
|
-
.filter(v => v && v.url)
|
|
288
|
-
.map(v => ({
|
|
284
|
+
return videoBackup.filter(v => v && v.url).map(v => ({
|
|
289
285
|
quality: v.quality || v.label || 'unknown',
|
|
290
286
|
url: v.url,
|
|
291
287
|
bit_rate: Number(v.bit_rate || 0)
|
|
292
|
-
}))
|
|
293
|
-
.sort((a, b) => b.bit_rate - a.bit_rate);
|
|
288
|
+
})).sort((a, b) => b.bit_rate - a.bit_rate);
|
|
294
289
|
}
|
|
295
290
|
function parseApiResponse(raw, maxDescLen) {
|
|
296
|
-
debugLog('DEBUG', '
|
|
291
|
+
debugLog('DEBUG', 'API raw response', raw);
|
|
297
292
|
const data = raw?.data || {};
|
|
298
293
|
const extra = data.extra || {};
|
|
299
294
|
let type = data.type || '';
|
|
@@ -333,31 +328,18 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
333
328
|
const validVideos = data.videos.filter((v) => v && v.url);
|
|
334
329
|
if (validVideos.length) {
|
|
335
330
|
video = validVideos[0].url;
|
|
336
|
-
videos = validVideos.map((v) => ({
|
|
337
|
-
quality: v.accept?.[0] || 'unknown',
|
|
338
|
-
url: v.url
|
|
339
|
-
}));
|
|
331
|
+
videos = validVideos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
|
|
340
332
|
}
|
|
341
333
|
}
|
|
342
|
-
if (!video && data.url)
|
|
334
|
+
if (!video && data.url)
|
|
343
335
|
video = data.url;
|
|
344
|
-
|
|
345
|
-
if (video && !video.startsWith('http')) {
|
|
336
|
+
if (video && !video.startsWith('http'))
|
|
346
337
|
video = 'https:' + video;
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return img;
|
|
353
|
-
})
|
|
354
|
-
: [];
|
|
355
|
-
const live_photo = Array.isArray(data.live_photo)
|
|
356
|
-
? data.live_photo.filter((lp) => lp && lp.image).map((lp) => ({
|
|
357
|
-
image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
|
|
358
|
-
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
359
|
-
}))
|
|
360
|
-
: [];
|
|
338
|
+
const images = Array.isArray(data.images) ? data.images.filter((img) => img && typeof img === 'string').map((img) => img.startsWith('http') ? img : 'https:' + img) : [];
|
|
339
|
+
const live_photo = Array.isArray(data.live_photo) ? data.live_photo.filter((lp) => lp && lp.image).map((lp) => ({
|
|
340
|
+
image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
|
|
341
|
+
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
342
|
+
})) : [];
|
|
361
343
|
const music = {
|
|
362
344
|
title: data.music?.title || data.music?.name || '',
|
|
363
345
|
author: data.music?.author || data.music?.artist || '',
|
|
@@ -388,12 +370,7 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
388
370
|
else if (extra.create_time) {
|
|
389
371
|
publishTime = extra.create_time * 1000;
|
|
390
372
|
}
|
|
391
|
-
return {
|
|
392
|
-
type, title, desc, author, uid, avatar, cover,
|
|
393
|
-
video, videos, images, live_photo, music,
|
|
394
|
-
like, comment, collect, share, play,
|
|
395
|
-
duration, publishTime
|
|
396
|
-
};
|
|
373
|
+
return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
|
|
397
374
|
}
|
|
398
375
|
const formatVarRegex = /\$\{([^}]+)\}/g;
|
|
399
376
|
function generateFormattedText(p, format) {
|
|
@@ -414,10 +391,6 @@ function generateFormattedText(p, format) {
|
|
|
414
391
|
'封面': p.cover,
|
|
415
392
|
'视频链接': p.video,
|
|
416
393
|
};
|
|
417
|
-
const varReplacements = Object.entries(vars).map(([key, val]) => ({
|
|
418
|
-
regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
|
|
419
|
-
value: val,
|
|
420
|
-
}));
|
|
421
394
|
const lines = format.split('\n');
|
|
422
395
|
const resultLines = [];
|
|
423
396
|
for (const line of lines) {
|
|
@@ -436,8 +409,8 @@ function generateFormattedText(p, format) {
|
|
|
436
409
|
continue;
|
|
437
410
|
}
|
|
438
411
|
let newLine = line;
|
|
439
|
-
for (const
|
|
440
|
-
newLine = newLine.replace(
|
|
412
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
413
|
+
newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
|
|
441
414
|
}
|
|
442
415
|
resultLines.push(newLine);
|
|
443
416
|
}
|
|
@@ -452,12 +425,7 @@ function buildForwardNode(session, content, botName) {
|
|
|
452
425
|
messageContent = [content];
|
|
453
426
|
else
|
|
454
427
|
messageContent = [koishi_1.h.text(String(content))];
|
|
455
|
-
return (0, koishi_1.h)('node', {
|
|
456
|
-
user: {
|
|
457
|
-
nickname: botName.substring(0, 15),
|
|
458
|
-
user_id: session.selfId
|
|
459
|
-
}
|
|
460
|
-
}, messageContent);
|
|
428
|
+
return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
|
|
461
429
|
}
|
|
462
430
|
function getErrorMessage(error) {
|
|
463
431
|
if (error instanceof Error)
|
|
@@ -468,7 +436,7 @@ function getErrorMessage(error) {
|
|
|
468
436
|
}
|
|
469
437
|
function apply(ctx, config) {
|
|
470
438
|
debugEnabled = config.debug || false;
|
|
471
|
-
debugLog('INFO', '
|
|
439
|
+
debugLog('INFO', 'plugin start');
|
|
472
440
|
const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
|
|
473
441
|
const texts = {
|
|
474
442
|
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
@@ -498,33 +466,47 @@ function apply(ctx, config) {
|
|
|
498
466
|
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
499
467
|
pipixia: 'https://api.bugpk.com/api/pipixia',
|
|
500
468
|
zuiyou: 'https://api.bugpk.com/api/zuiyou',
|
|
469
|
+
wechat_channel: 'https://api.bugpk.com/api/wxsph',
|
|
501
470
|
};
|
|
502
471
|
const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
|
|
503
472
|
function getPlatformConfig(type) {
|
|
504
473
|
const custom = config.customApis?.find((item) => item.platform === type);
|
|
505
474
|
let apiUrl = defaultDedicatedApis[type] || null;
|
|
475
|
+
let apiKey = '';
|
|
476
|
+
let authHeaderType = 'Bearer';
|
|
477
|
+
let customHeaderName = 'X-API-Key';
|
|
506
478
|
if (custom && custom.apiUrl) {
|
|
507
479
|
apiUrl = custom.apiUrl;
|
|
480
|
+
apiKey = custom.apiKey || '';
|
|
481
|
+
authHeaderType = custom.authHeaderType || 'Bearer';
|
|
482
|
+
customHeaderName = custom.customHeaderName || 'X-API-Key';
|
|
508
483
|
}
|
|
509
484
|
const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
|
|
510
|
-
return { apiUrl, dedicatedFirst };
|
|
485
|
+
return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName };
|
|
486
|
+
}
|
|
487
|
+
function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
|
|
488
|
+
if (!apiKey)
|
|
489
|
+
return {};
|
|
490
|
+
if (authHeaderType === 'Bearer')
|
|
491
|
+
return { 'Authorization': `Bearer ${apiKey}` };
|
|
492
|
+
if (authHeaderType === 'X-API-Key')
|
|
493
|
+
return { 'X-API-Key': apiKey };
|
|
494
|
+
if (authHeaderType === 'Custom' && customHeaderName)
|
|
495
|
+
return { [customHeaderName]: apiKey };
|
|
496
|
+
return {};
|
|
511
497
|
}
|
|
512
498
|
async function resolveShortUrl(url) {
|
|
513
499
|
try {
|
|
514
500
|
const res = await http.get(url, {
|
|
515
501
|
timeout: 10000,
|
|
516
502
|
maxRedirects: 10,
|
|
517
|
-
headers: {
|
|
518
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
519
|
-
'Referer': 'https://www.baidu.com/',
|
|
520
|
-
},
|
|
503
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
|
|
521
504
|
validateStatus: (status) => status >= 200 && status < 400,
|
|
522
505
|
});
|
|
523
506
|
const finalUrl = res.request?.res?.responseUrl || url;
|
|
524
507
|
return cleanUrl(finalUrl);
|
|
525
508
|
}
|
|
526
|
-
catch
|
|
527
|
-
debugLog('WARN', '解析短链接失败:', e, '原始URL:', url);
|
|
509
|
+
catch {
|
|
528
510
|
return cleanUrl(url);
|
|
529
511
|
}
|
|
530
512
|
}
|
|
@@ -535,8 +517,6 @@ function apply(ctx, config) {
|
|
|
535
517
|
await promises_1.default.mkdir(tempDir, { recursive: true });
|
|
536
518
|
const fileName = `video_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.mp4`;
|
|
537
519
|
const filePath = path_1.default.resolve(tempDir, fileName);
|
|
538
|
-
debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
|
|
539
|
-
debugLog('INFO', `临时文件路径: ${filePath}`);
|
|
540
520
|
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
541
521
|
let response;
|
|
542
522
|
try {
|
|
@@ -545,10 +525,7 @@ function apply(ctx, config) {
|
|
|
545
525
|
url: videoUrl,
|
|
546
526
|
responseType: 'stream',
|
|
547
527
|
timeout: config.videoDownloadTimeout || 120000,
|
|
548
|
-
headers: {
|
|
549
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
550
|
-
'Referer': 'https://www.bilibili.com/',
|
|
551
|
-
},
|
|
528
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
|
|
552
529
|
maxRedirects: 5,
|
|
553
530
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
554
531
|
});
|
|
@@ -567,7 +544,6 @@ function apply(ctx, config) {
|
|
|
567
544
|
}
|
|
568
545
|
try {
|
|
569
546
|
await (0, promises_2.pipeline)(response.data, writer);
|
|
570
|
-
debugLog('INFO', `视频下载完成`);
|
|
571
547
|
return filePath;
|
|
572
548
|
}
|
|
573
549
|
catch (e) {
|
|
@@ -578,17 +554,15 @@ function apply(ctx, config) {
|
|
|
578
554
|
async function fetchApi(url, type) {
|
|
579
555
|
const cacheKey = url;
|
|
580
556
|
const cached = urlCache.get(cacheKey);
|
|
581
|
-
if (cached && cached.expire > Date.now())
|
|
582
|
-
debugLog('DEBUG', `使用缓存: ${url}`);
|
|
557
|
+
if (cached && cached.expire > Date.now())
|
|
583
558
|
return cached.data;
|
|
584
|
-
}
|
|
585
|
-
const { apiUrl: dedicatedUrl, dedicatedFirst } = getPlatformConfig(type);
|
|
559
|
+
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
|
|
586
560
|
const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
|
|
587
561
|
const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
|
|
588
562
|
const backupAllowed = backupSupportedPlatforms.has(type);
|
|
589
563
|
const apiList = [];
|
|
590
564
|
if (dedicatedFirst && dedicatedUrl) {
|
|
591
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})
|
|
565
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
592
566
|
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
593
567
|
if (backupAllowed)
|
|
594
568
|
apiList.push({ url: backupApi, label: '备用主API' });
|
|
@@ -598,16 +572,22 @@ function apply(ctx, config) {
|
|
|
598
572
|
if (backupAllowed)
|
|
599
573
|
apiList.push({ url: backupApi, label: '备用主API' });
|
|
600
574
|
if (dedicatedUrl)
|
|
601
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})
|
|
575
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
602
576
|
}
|
|
603
577
|
let lastError = null;
|
|
604
578
|
for (const api of apiList) {
|
|
605
579
|
for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
|
|
606
580
|
try {
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
581
|
+
const headers = {
|
|
582
|
+
'User-Agent': config.userAgent,
|
|
583
|
+
'Referer': 'https://www.baidu.com/',
|
|
584
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
585
|
+
};
|
|
586
|
+
if (api.apiKey) {
|
|
587
|
+
const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
|
|
588
|
+
Object.assign(headers, authHeaders);
|
|
589
|
+
}
|
|
590
|
+
const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
|
|
611
591
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
612
592
|
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
613
593
|
urlCache.set(cacheKey, { data: parsed, expire: Date.now() + 10 * 60 * 1000 });
|
|
@@ -617,81 +597,59 @@ function apply(ctx, config) {
|
|
|
617
597
|
}
|
|
618
598
|
catch (error) {
|
|
619
599
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
620
|
-
debugLog('ERROR', `${api.label}
|
|
621
|
-
if (attempt < config.retryTimes)
|
|
600
|
+
debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
|
|
601
|
+
if (attempt < config.retryTimes)
|
|
622
602
|
await delay(config.retryInterval);
|
|
623
|
-
}
|
|
624
603
|
}
|
|
625
604
|
}
|
|
626
|
-
debugLog('WARN', `${api.label}
|
|
605
|
+
debugLog('WARN', `${api.label} all retries failed`);
|
|
627
606
|
}
|
|
628
607
|
throw lastError || new Error('所有API请求全部失败');
|
|
629
608
|
}
|
|
630
609
|
async function parseUrl(url, type) {
|
|
631
610
|
const realUrl = await resolveShortUrl(url);
|
|
632
|
-
const candidates = [realUrl, url];
|
|
633
|
-
for (const candidate of
|
|
611
|
+
const candidates = [...new Set([realUrl, url])];
|
|
612
|
+
for (const candidate of candidates) {
|
|
634
613
|
try {
|
|
635
614
|
const info = await fetchApi(candidate, type);
|
|
636
|
-
if (info.video || info.images.length > 0)
|
|
615
|
+
if (info.video || info.images.length > 0)
|
|
637
616
|
return { success: true, data: info };
|
|
638
|
-
}
|
|
639
|
-
debugLog('WARN', `解析成功但无有效内容: ${candidate}`);
|
|
617
|
+
debugLog('WARN', `解析成功但无内容: ${candidate}`);
|
|
640
618
|
}
|
|
641
619
|
catch (error) {
|
|
642
|
-
debugLog('ERROR',
|
|
620
|
+
debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
|
|
643
621
|
}
|
|
644
622
|
}
|
|
645
623
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
646
624
|
}
|
|
647
625
|
async function processSingleUrl(url, type) {
|
|
648
626
|
const result = await parseUrl(url, type);
|
|
649
|
-
if (!result.success)
|
|
627
|
+
if (!result.success)
|
|
650
628
|
return { success: false, msg: result.msg, url };
|
|
651
|
-
}
|
|
652
629
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
653
|
-
return {
|
|
654
|
-
success: true,
|
|
655
|
-
data: {
|
|
656
|
-
text,
|
|
657
|
-
parsed: result.data
|
|
658
|
-
}
|
|
659
|
-
};
|
|
630
|
+
return { success: true, data: { text, parsed: result.data } };
|
|
660
631
|
}
|
|
661
632
|
async function sendWithTimeout(session, content, customRetries) {
|
|
662
633
|
const maxRetries = customRetries ?? config.retryTimes ?? 3;
|
|
663
634
|
const retryDelay = config.retryInterval || 1000;
|
|
664
|
-
let timeoutId = null;
|
|
665
635
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
666
636
|
try {
|
|
667
637
|
let sendPromise = session.send(content);
|
|
668
638
|
if (config.videoSendTimeout > 0) {
|
|
669
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
670
|
-
|
|
671
|
-
});
|
|
672
|
-
const result = await Promise.race([sendPromise, timeoutPromise]);
|
|
673
|
-
if (timeoutId)
|
|
674
|
-
clearTimeout(timeoutId);
|
|
675
|
-
return result;
|
|
639
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout));
|
|
640
|
+
return await Promise.race([sendPromise, timeoutPromise]);
|
|
676
641
|
}
|
|
677
642
|
else {
|
|
678
643
|
return await sendPromise;
|
|
679
644
|
}
|
|
680
645
|
}
|
|
681
646
|
catch (err) {
|
|
682
|
-
if (timeoutId)
|
|
683
|
-
clearTimeout(timeoutId);
|
|
684
647
|
const errMsg = getErrorMessage(err);
|
|
685
|
-
debugLog('ERROR',
|
|
686
|
-
if (attempt < maxRetries)
|
|
687
|
-
debugLog('INFO', `等待 ${retryDelay}ms 后进行第 ${attempt + 2} 次重试`);
|
|
648
|
+
debugLog('ERROR', `发送失败尝试 ${attempt + 1}: ${errMsg}`);
|
|
649
|
+
if (attempt < maxRetries)
|
|
688
650
|
await delay(retryDelay);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (!config.ignoreSendError)
|
|
692
|
-
throw err;
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
651
|
+
else if (!config.ignoreSendError)
|
|
652
|
+
throw err;
|
|
695
653
|
}
|
|
696
654
|
}
|
|
697
655
|
return null;
|
|
@@ -699,47 +657,36 @@ function apply(ctx, config) {
|
|
|
699
657
|
async function sendVideoFile(session, videoUrl) {
|
|
700
658
|
if (!videoUrl)
|
|
701
659
|
return;
|
|
702
|
-
if (!config.showVideoFile)
|
|
660
|
+
if (!config.showVideoFile)
|
|
703
661
|
return await sendWithTimeout(session, `视频链接:${videoUrl}`);
|
|
704
|
-
}
|
|
705
|
-
const sendLink = async () => {
|
|
706
|
-
await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { });
|
|
707
|
-
};
|
|
662
|
+
const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
|
|
708
663
|
if (config.forceDownloadVideo) {
|
|
709
664
|
try {
|
|
710
665
|
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
711
|
-
|
|
712
|
-
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
666
|
+
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
713
667
|
return;
|
|
714
668
|
}
|
|
715
669
|
catch (e) {
|
|
716
|
-
debugLog('ERROR', '
|
|
670
|
+
debugLog('ERROR', '强制下载失败,尝试URL发送:', getErrorMessage(e));
|
|
717
671
|
try {
|
|
718
672
|
await sendWithTimeout(session, koishi_1.h.video(videoUrl));
|
|
719
673
|
return;
|
|
720
674
|
}
|
|
721
|
-
catch
|
|
722
|
-
debugLog('ERROR', '发送URL也失败,降级发送链接:', getErrorMessage(urlErr));
|
|
675
|
+
catch {
|
|
723
676
|
await sendLink();
|
|
724
677
|
}
|
|
725
678
|
}
|
|
726
679
|
return;
|
|
727
680
|
}
|
|
728
681
|
try {
|
|
729
|
-
debugLog('INFO', '尝试直接发送视频URL');
|
|
730
682
|
await sendWithTimeout(session, koishi_1.h.video(videoUrl));
|
|
731
|
-
return;
|
|
732
683
|
}
|
|
733
|
-
catch
|
|
734
|
-
debugLog('ERROR', '直接发送URL失败,尝试下载:', getErrorMessage(urlErr));
|
|
684
|
+
catch {
|
|
735
685
|
try {
|
|
736
686
|
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
737
|
-
|
|
738
|
-
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
739
|
-
return;
|
|
687
|
+
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
740
688
|
}
|
|
741
|
-
catch
|
|
742
|
-
debugLog('ERROR', '下载失败,降级发送链接:', getErrorMessage(downloadErr));
|
|
689
|
+
catch {
|
|
743
690
|
await sendLink();
|
|
744
691
|
}
|
|
745
692
|
}
|
|
@@ -755,37 +702,28 @@ function apply(ctx, config) {
|
|
|
755
702
|
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
756
703
|
debugLog('INFO', `跳过重复链接: ${match.url}`);
|
|
757
704
|
const shortUrl = match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url;
|
|
758
|
-
|
|
759
|
-
await sendWithTimeout(session, skipMsg).catch(() => { });
|
|
705
|
+
await sendWithTimeout(session, `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`).catch(() => { });
|
|
760
706
|
continue;
|
|
761
707
|
}
|
|
762
708
|
}
|
|
763
|
-
debugLog('INFO',
|
|
709
|
+
debugLog('INFO', `解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (${match.type})`);
|
|
764
710
|
const result = await processSingleUrl(match.url, match.type);
|
|
765
711
|
if (result.success) {
|
|
766
712
|
items.push(result.data);
|
|
767
|
-
if (config.deduplicationInterval > 0)
|
|
713
|
+
if (config.deduplicationInterval > 0)
|
|
768
714
|
dedupCache.set(match.url, Date.now());
|
|
769
|
-
}
|
|
770
715
|
}
|
|
771
716
|
else {
|
|
772
|
-
const item = texts.parseErrorItemFormat
|
|
773
|
-
.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url)
|
|
774
|
-
.replace(/\$\{msg\}/g, result.msg);
|
|
717
|
+
const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
|
|
775
718
|
errors.push(item);
|
|
776
719
|
}
|
|
777
|
-
if (i < matches.length - 1)
|
|
720
|
+
if (i < matches.length - 1)
|
|
778
721
|
await delay(500);
|
|
779
|
-
}
|
|
780
722
|
}
|
|
781
|
-
if (errors.length)
|
|
723
|
+
if (errors.length)
|
|
782
724
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
if (!items.length) {
|
|
786
|
-
debugLog('INFO', '没有成功解析的内容');
|
|
725
|
+
if (!items.length)
|
|
787
726
|
return;
|
|
788
|
-
}
|
|
789
727
|
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
790
728
|
const botName = config.botName || '视频解析机器人';
|
|
791
729
|
if (enableForward) {
|
|
@@ -793,30 +731,24 @@ function apply(ctx, config) {
|
|
|
793
731
|
for (const item of items) {
|
|
794
732
|
const p = item.parsed;
|
|
795
733
|
const text = item.text;
|
|
796
|
-
if (text && config.showImageText)
|
|
734
|
+
if (text && config.showImageText)
|
|
797
735
|
forwardMessages.push(buildForwardNode(session, text, botName));
|
|
798
|
-
|
|
799
|
-
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
736
|
+
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
|
|
800
737
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
801
|
-
}
|
|
802
738
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
803
739
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
804
|
-
for (const imgUrl of imageUrls)
|
|
740
|
+
for (const imgUrl of imageUrls)
|
|
805
741
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
|
|
806
|
-
}
|
|
807
742
|
}
|
|
808
|
-
if (p.video)
|
|
743
|
+
if (p.video)
|
|
809
744
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
|
|
810
|
-
}
|
|
811
745
|
}
|
|
812
746
|
if (forwardMessages.length) {
|
|
813
|
-
const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
|
|
814
747
|
try {
|
|
815
|
-
|
|
816
|
-
await sendWithTimeout(session, forwardMsg, config.retryTimes);
|
|
748
|
+
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)), config.retryTimes);
|
|
817
749
|
}
|
|
818
750
|
catch (err) {
|
|
819
|
-
debugLog('ERROR', '
|
|
751
|
+
debugLog('ERROR', '合并转发失败,降级逐条发送:', err);
|
|
820
752
|
for (const node of forwardMessages) {
|
|
821
753
|
await sendWithTimeout(session, node.data.content).catch(() => { });
|
|
822
754
|
await delay(300);
|
|
@@ -837,17 +769,10 @@ function apply(ctx, config) {
|
|
|
837
769
|
await delay(300);
|
|
838
770
|
}
|
|
839
771
|
if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
840
|
-
if (config.showVideoFile)
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
catch (e) {
|
|
845
|
-
debugLog('ERROR', `视频发送失败: ${getErrorMessage(e)}`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
else {
|
|
772
|
+
if (config.showVideoFile)
|
|
773
|
+
await sendVideoFile(session, p.video);
|
|
774
|
+
else
|
|
849
775
|
await sendWithTimeout(session, `视频链接:${p.video}`);
|
|
850
|
-
}
|
|
851
776
|
await delay(500);
|
|
852
777
|
}
|
|
853
778
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
@@ -859,7 +784,7 @@ function apply(ctx, config) {
|
|
|
859
784
|
}
|
|
860
785
|
}
|
|
861
786
|
}
|
|
862
|
-
debugLog('INFO', '
|
|
787
|
+
debugLog('INFO', '处理完成');
|
|
863
788
|
}
|
|
864
789
|
ctx.on('message', async (session) => {
|
|
865
790
|
if (!config.enable)
|
|
@@ -873,13 +798,13 @@ function apply(ctx, config) {
|
|
|
873
798
|
const matches = extractAllUrlsFromMessage(session);
|
|
874
799
|
if (!matches.length)
|
|
875
800
|
return;
|
|
876
|
-
debugLog('INFO', `检测到 ${matches.length}
|
|
801
|
+
debugLog('INFO', `检测到 ${matches.length} 个链接`);
|
|
877
802
|
if (config.showWaitingTip) {
|
|
878
803
|
try {
|
|
879
804
|
await sendWithTimeout(session, texts.waitingTipText);
|
|
880
805
|
}
|
|
881
806
|
catch (e) {
|
|
882
|
-
debugLog('WARN', '
|
|
807
|
+
debugLog('WARN', '等待提示发送失败:', e);
|
|
883
808
|
}
|
|
884
809
|
}
|
|
885
810
|
await flush(session, matches);
|
|
@@ -907,20 +832,19 @@ function apply(ctx, config) {
|
|
|
907
832
|
const tempDir = config.tempDir || './temp_videos';
|
|
908
833
|
const files = await promises_1.default.readdir(tempDir);
|
|
909
834
|
const now = Date.now();
|
|
910
|
-
let
|
|
835
|
+
let deleted = 0;
|
|
911
836
|
for (const file of files) {
|
|
912
837
|
if (file.startsWith('video_') && file.endsWith('.mp4')) {
|
|
913
838
|
const filePath = path_1.default.join(tempDir, file);
|
|
914
839
|
const stats = await promises_1.default.stat(filePath);
|
|
915
840
|
if (now - stats.mtimeMs > 3600000) {
|
|
916
841
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
917
|
-
|
|
842
|
+
deleted++;
|
|
918
843
|
}
|
|
919
844
|
}
|
|
920
845
|
}
|
|
921
|
-
if (
|
|
922
|
-
debugLog('INFO', `清理了 ${
|
|
923
|
-
}
|
|
846
|
+
if (deleted)
|
|
847
|
+
debugLog('INFO', `清理了 ${deleted} 个过期临时视频文件`);
|
|
924
848
|
}
|
|
925
849
|
catch (e) {
|
|
926
850
|
debugLog('WARN', '清理临时文件失败:', e);
|
|
@@ -930,18 +854,16 @@ function apply(ctx, config) {
|
|
|
930
854
|
clearInterval(tempCleanupInterval);
|
|
931
855
|
urlCache.clear();
|
|
932
856
|
dedupCache.clear();
|
|
933
|
-
debugLog('INFO', '
|
|
857
|
+
debugLog('INFO', '插件已卸载');
|
|
934
858
|
});
|
|
935
859
|
process.on('beforeExit', async () => {
|
|
936
860
|
try {
|
|
937
861
|
const tempDir = config.tempDir || './temp_videos';
|
|
938
862
|
const files = await promises_1.default.readdir(tempDir);
|
|
939
863
|
for (const file of files) {
|
|
940
|
-
if (file.startsWith('video_') && file.endsWith('.mp4'))
|
|
864
|
+
if (file.startsWith('video_') && file.endsWith('.mp4'))
|
|
941
865
|
await promises_1.default.unlink(path_1.default.join(tempDir, file)).catch(() => { });
|
|
942
|
-
}
|
|
943
866
|
}
|
|
944
|
-
debugLog('INFO', '进程退出,已清理所有临时视频文件');
|
|
945
867
|
}
|
|
946
868
|
catch { }
|
|
947
869
|
});
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
5
|
### 中文
|
|
6
|
-
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun
|
|
6
|
+
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙、绿洲、视频号等20+主流平台的短视频/图集/实况链接。
|
|
7
7
|
|
|
8
8
|
### English
|
|
9
|
-
This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image/live photo links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya and more.
|
|
9
|
+
This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image/live photo links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya, Oasis, WeChat Channels and more.
|
|
10
10
|
|
|
11
11
|
## 项目仓库 (Repository)
|
|
12
12
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
@@ -56,8 +56,8 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
56
56
|
|--------|------|--------|------|
|
|
57
57
|
| `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址,解析时优先使用 |
|
|
58
58
|
| `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API 地址,仅支持抖音、小红书、Instagram、即梦平台解析 |
|
|
59
|
-
| `platformDedicatedFirst` | object | 各平台均为 `false` | 各平台独立开关:是否优先使用平台专属 API。对象键为平台标识(英文),值为布尔值。支持的键:`bilibili
|
|
60
|
-
| `customApis` | array | [] | 自定义平台专属 API 列表。每项包含:`platform`(平台类型)、`apiUrl`(API
|
|
59
|
+
| `platformDedicatedFirst` | object | 各平台均为 `false` | 各平台独立开关:是否优先使用平台专属 API。对象键为平台标识(英文),值为布尔值。支持的键:`bilibili`、`douyin`、`kuaishou`、`xiaohongshu`、`weibo`、`xigua`、`youtube`、`tiktok`、`acfun`、`zhihu`、`weishi`、`huya`、`haokan`、`meipai`、`twitter`、`instagram`、`doubao`、`oasis`、`wechat_channel` |
|
|
60
|
+
| `customApis` | array | [] | 自定义平台专属 API 列表。每项包含:`platform`(平台类型)、`apiUrl`(API 地址)、`apiKey`(API Key,可选)、`authHeaderType`(认证头类型,可选:`Bearer` / `X-API-Key` / `Custom`)、`customHeaderName`(自定义 Header 名称,仅当 `authHeaderType` 为 `Custom` 时有效)。可覆盖内置默认专属 API |
|
|
61
61
|
|
|
62
62
|
### 错误与重试设置
|
|
63
63
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -133,6 +133,8 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
133
133
|
| 皮皮搞笑 | pipigx, h5.pipigx.com | 短视频 |
|
|
134
134
|
| 皮皮虾 | pipixia, h5.pipix.com | 短视频 |
|
|
135
135
|
| 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
|
|
136
|
+
| 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
|
|
137
|
+
| 视频号 (WeChat Channels) | channels.weixin.qq.com | 短视频 |
|
|
136
138
|
|
|
137
139
|
> 注:部分平台解析能力可能因API限制有所差异,具体以实际解析结果为准。
|
|
138
140
|
|