koishi-plugin-video-parser-all 1.2.5 → 1.2.6
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 +159 -249
- 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,55 +80,54 @@ 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)
|
|
84
|
-
douyin: koishi_1.Schema.boolean().default(false)
|
|
85
|
-
kuaishou: koishi_1.Schema.boolean().default(false)
|
|
86
|
-
xiaohongshu: koishi_1.Schema.boolean().default(false)
|
|
87
|
-
weibo: koishi_1.Schema.boolean().default(false)
|
|
88
|
-
xigua: koishi_1.Schema.boolean().default(false)
|
|
89
|
-
youtube: koishi_1.Schema.boolean().default(false)
|
|
90
|
-
tiktok: koishi_1.Schema.boolean().default(false)
|
|
91
|
-
acfun: koishi_1.Schema.boolean().default(false)
|
|
92
|
-
zhihu: koishi_1.Schema.boolean().default(false)
|
|
93
|
-
weishi: koishi_1.Schema.boolean().default(false)
|
|
94
|
-
huya: koishi_1.Schema.boolean().default(false)
|
|
95
|
-
haokan: koishi_1.Schema.boolean().default(false)
|
|
96
|
-
meipai: koishi_1.Schema.boolean().default(false)
|
|
97
|
-
twitter: koishi_1.Schema.boolean().default(false)
|
|
98
|
-
instagram: koishi_1.Schema.boolean().default(false)
|
|
99
|
-
doubao: koishi_1.Schema.boolean().default(false)
|
|
85
|
+
bilibili: koishi_1.Schema.boolean().default(false),
|
|
86
|
+
douyin: koishi_1.Schema.boolean().default(false),
|
|
87
|
+
kuaishou: koishi_1.Schema.boolean().default(false),
|
|
88
|
+
xiaohongshu: koishi_1.Schema.boolean().default(false),
|
|
89
|
+
weibo: koishi_1.Schema.boolean().default(false),
|
|
90
|
+
xigua: koishi_1.Schema.boolean().default(false),
|
|
91
|
+
youtube: koishi_1.Schema.boolean().default(false),
|
|
92
|
+
tiktok: koishi_1.Schema.boolean().default(false),
|
|
93
|
+
acfun: koishi_1.Schema.boolean().default(false),
|
|
94
|
+
zhihu: koishi_1.Schema.boolean().default(false),
|
|
95
|
+
weishi: koishi_1.Schema.boolean().default(false),
|
|
96
|
+
huya: koishi_1.Schema.boolean().default(false),
|
|
97
|
+
haokan: koishi_1.Schema.boolean().default(false),
|
|
98
|
+
meipai: koishi_1.Schema.boolean().default(false),
|
|
99
|
+
twitter: koishi_1.Schema.boolean().default(false),
|
|
100
|
+
instagram: koishi_1.Schema.boolean().default(false),
|
|
101
|
+
doubao: koishi_1.Schema.boolean().default(false),
|
|
102
|
+
oasis: koishi_1.Schema.boolean().default(false),
|
|
103
|
+
wechat_channel: koishi_1.Schema.boolean().default(false),
|
|
100
104
|
}).description('各平台独立开关:是否优先使用专属 API'),
|
|
101
105
|
customApis: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
102
106
|
platform: koishi_1.Schema.union([
|
|
103
|
-
koishi_1.Schema.const('bilibili').
|
|
104
|
-
koishi_1.Schema.const('
|
|
105
|
-
koishi_1.Schema.const('
|
|
106
|
-
koishi_1.Schema.const('
|
|
107
|
-
koishi_1.Schema.const('
|
|
108
|
-
koishi_1.Schema.const('
|
|
109
|
-
koishi_1.Schema.const('
|
|
110
|
-
koishi_1.Schema.const('tiktok').description('TikTok'),
|
|
111
|
-
koishi_1.Schema.const('acfun').description('AcFun'),
|
|
112
|
-
koishi_1.Schema.const('zhihu').description('知乎'),
|
|
113
|
-
koishi_1.Schema.const('weishi').description('微视'),
|
|
114
|
-
koishi_1.Schema.const('huya').description('虎牙'),
|
|
115
|
-
koishi_1.Schema.const('haokan').description('好看视频'),
|
|
116
|
-
koishi_1.Schema.const('meipai').description('美拍'),
|
|
117
|
-
koishi_1.Schema.const('twitter').description('Twitter/X'),
|
|
118
|
-
koishi_1.Schema.const('instagram').description('Instagram'),
|
|
119
|
-
koishi_1.Schema.const('doubao').description('豆包'),
|
|
107
|
+
koishi_1.Schema.const('bilibili'), koishi_1.Schema.const('douyin'), koishi_1.Schema.const('kuaishou'),
|
|
108
|
+
koishi_1.Schema.const('xiaohongshu'), koishi_1.Schema.const('weibo'), koishi_1.Schema.const('xigua'),
|
|
109
|
+
koishi_1.Schema.const('youtube'), koishi_1.Schema.const('tiktok'), koishi_1.Schema.const('acfun'),
|
|
110
|
+
koishi_1.Schema.const('zhihu'), koishi_1.Schema.const('weishi'), koishi_1.Schema.const('huya'),
|
|
111
|
+
koishi_1.Schema.const('haokan'), koishi_1.Schema.const('meipai'), koishi_1.Schema.const('twitter'),
|
|
112
|
+
koishi_1.Schema.const('instagram'), koishi_1.Schema.const('doubao'), koishi_1.Schema.const('oasis'),
|
|
113
|
+
koishi_1.Schema.const('wechat_channel'),
|
|
120
114
|
]).description('选择平台'),
|
|
121
115
|
apiUrl: koishi_1.Schema.string().description('API 地址'),
|
|
122
|
-
|
|
116
|
+
apiKey: koishi_1.Schema.string().description('API Key(可选)').default(''),
|
|
117
|
+
authHeaderType: koishi_1.Schema.union([
|
|
118
|
+
koishi_1.Schema.const('Bearer').description('Bearer Token'),
|
|
119
|
+
koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
|
|
120
|
+
koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
|
|
121
|
+
]).default('Bearer').description('认证头类型'),
|
|
122
|
+
customHeaderName: koishi_1.Schema.string().description('自定义 Header 名称(仅当选择 Custom 时有效)').default('X-API-Key'),
|
|
123
|
+
})).default([]).description('自定义平台专属 API 地址'),
|
|
123
124
|
}).description('API 选择设置'),
|
|
124
125
|
koishi_1.Schema.object({
|
|
125
|
-
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...')
|
|
126
|
-
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接')
|
|
127
|
-
invalidLinkText: koishi_1.Schema.string().default('无效的视频链接')
|
|
128
|
-
parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:')
|
|
129
|
-
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}')
|
|
126
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...'),
|
|
127
|
+
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接'),
|
|
128
|
+
invalidLinkText: koishi_1.Schema.string().default('无效的视频链接'),
|
|
129
|
+
parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:'),
|
|
130
|
+
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}'),
|
|
130
131
|
}).description('界面文字设置'),
|
|
131
132
|
]);
|
|
132
133
|
const logger = new koishi_1.Logger(exports.name);
|
|
@@ -134,19 +135,7 @@ let debugEnabled = false;
|
|
|
134
135
|
function debugLog(level, ...args) {
|
|
135
136
|
if (!debugEnabled)
|
|
136
137
|
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);
|
|
138
|
+
logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
|
|
150
139
|
}
|
|
151
140
|
const urlCache = new SimpleLRUCache(500, 10 * 60 * 1000);
|
|
152
141
|
const LINK_RULES = [
|
|
@@ -176,6 +165,8 @@ const LINK_RULES = [
|
|
|
176
165
|
{ pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
|
|
177
166
|
{ pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
|
|
178
167
|
{ pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
|
|
168
|
+
{ pattern: /https?:\/\/(?:www\.)?oasis\.weibo\.com\/v\/[0-9a-zA-Z_-]+/gi, type: 'oasis' },
|
|
169
|
+
{ pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
179
170
|
];
|
|
180
171
|
function linkTypeParser(content) {
|
|
181
172
|
content = content.replace(/\\\//g, '/');
|
|
@@ -200,9 +191,8 @@ function extractAllUrlsFromMessage(session) {
|
|
|
200
191
|
const cardsContent = [];
|
|
201
192
|
if (session.elements) {
|
|
202
193
|
for (const elem of session.elements) {
|
|
203
|
-
if (elem.type === 'xml' && elem.data)
|
|
194
|
+
if (elem.type === 'xml' && elem.data)
|
|
204
195
|
cardsContent.push(elem.data);
|
|
205
|
-
}
|
|
206
196
|
else if (elem.type === 'json' && elem.data) {
|
|
207
197
|
try {
|
|
208
198
|
const json = JSON.parse(elem.data);
|
|
@@ -210,9 +200,8 @@ function extractAllUrlsFromMessage(session) {
|
|
|
210
200
|
if (!obj || typeof obj !== 'object')
|
|
211
201
|
return;
|
|
212
202
|
for (const val of Object.values(obj)) {
|
|
213
|
-
if (typeof val === 'string')
|
|
203
|
+
if (typeof val === 'string')
|
|
214
204
|
cardsContent.push(val);
|
|
215
|
-
}
|
|
216
205
|
else if (typeof val === 'object')
|
|
217
206
|
extract(val);
|
|
218
207
|
}
|
|
@@ -224,8 +213,7 @@ function extractAllUrlsFromMessage(session) {
|
|
|
224
213
|
}
|
|
225
214
|
}
|
|
226
215
|
for (const cardContent of cardsContent) {
|
|
227
|
-
|
|
228
|
-
matchedLinks.push(...cardLinks);
|
|
216
|
+
matchedLinks.push(...linkTypeParser(cardContent));
|
|
229
217
|
}
|
|
230
218
|
const seen = new Set();
|
|
231
219
|
const result = [];
|
|
@@ -241,25 +229,19 @@ function cleanUrl(url) {
|
|
|
241
229
|
try {
|
|
242
230
|
url = url.replace(/&/g, '&');
|
|
243
231
|
const urlObj = new URL(url);
|
|
244
|
-
if (urlObj.protocol === 'http:')
|
|
232
|
+
if (urlObj.protocol === 'http:')
|
|
245
233
|
urlObj.protocol = 'https:';
|
|
246
|
-
}
|
|
247
234
|
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
|
-
});
|
|
235
|
+
['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => urlObj.searchParams.delete(p));
|
|
251
236
|
return urlObj.origin + urlObj.pathname;
|
|
252
237
|
}
|
|
253
238
|
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
|
-
});
|
|
239
|
+
['share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'timestamp'].forEach(p => urlObj.searchParams.delete(p));
|
|
257
240
|
return urlObj.origin + urlObj.pathname;
|
|
258
241
|
}
|
|
259
242
|
return urlObj.toString();
|
|
260
243
|
}
|
|
261
|
-
catch
|
|
262
|
-
debugLog('WARN', '清理URL失败:', e, '原始URL:', url);
|
|
244
|
+
catch {
|
|
263
245
|
return url.replace(/&/g, '&').replace(/\?.*/, '');
|
|
264
246
|
}
|
|
265
247
|
}
|
|
@@ -277,23 +259,24 @@ function formatPublishTime(ms) {
|
|
|
277
259
|
if (!ms)
|
|
278
260
|
return '';
|
|
279
261
|
const d = new Date(ms);
|
|
280
|
-
const y = d.getFullYear()
|
|
262
|
+
const y = d.getFullYear();
|
|
263
|
+
const mo = (d.getMonth() + 1).toString().padStart(2, '0');
|
|
264
|
+
const day = d.getDate().toString().padStart(2, '0');
|
|
265
|
+
const H = d.getHours().toString().padStart(2, '0');
|
|
266
|
+
const i = d.getMinutes().toString().padStart(2, '0');
|
|
281
267
|
return `${y}年${mo}月${day}日 ${H}:${i}`;
|
|
282
268
|
}
|
|
283
269
|
function pickBestQuality(videoBackup) {
|
|
284
270
|
if (!Array.isArray(videoBackup))
|
|
285
271
|
return [];
|
|
286
|
-
return videoBackup
|
|
287
|
-
.filter(v => v && v.url)
|
|
288
|
-
.map(v => ({
|
|
272
|
+
return videoBackup.filter(v => v && v.url).map(v => ({
|
|
289
273
|
quality: v.quality || v.label || 'unknown',
|
|
290
274
|
url: v.url,
|
|
291
275
|
bit_rate: Number(v.bit_rate || 0)
|
|
292
|
-
}))
|
|
293
|
-
.sort((a, b) => b.bit_rate - a.bit_rate);
|
|
276
|
+
})).sort((a, b) => b.bit_rate - a.bit_rate);
|
|
294
277
|
}
|
|
295
278
|
function parseApiResponse(raw, maxDescLen) {
|
|
296
|
-
debugLog('DEBUG', '
|
|
279
|
+
debugLog('DEBUG', 'API raw response', raw);
|
|
297
280
|
const data = raw?.data || {};
|
|
298
281
|
const extra = data.extra || {};
|
|
299
282
|
let type = data.type || '';
|
|
@@ -333,31 +316,18 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
333
316
|
const validVideos = data.videos.filter((v) => v && v.url);
|
|
334
317
|
if (validVideos.length) {
|
|
335
318
|
video = validVideos[0].url;
|
|
336
|
-
videos = validVideos.map((v) => ({
|
|
337
|
-
quality: v.accept?.[0] || 'unknown',
|
|
338
|
-
url: v.url
|
|
339
|
-
}));
|
|
319
|
+
videos = validVideos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
|
|
340
320
|
}
|
|
341
321
|
}
|
|
342
|
-
if (!video && data.url)
|
|
322
|
+
if (!video && data.url)
|
|
343
323
|
video = data.url;
|
|
344
|
-
|
|
345
|
-
if (video && !video.startsWith('http')) {
|
|
324
|
+
if (video && !video.startsWith('http'))
|
|
346
325
|
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
|
-
: [];
|
|
326
|
+
const images = Array.isArray(data.images) ? data.images.filter((img) => img && typeof img === 'string').map((img) => img.startsWith('http') ? img : 'https:' + img) : [];
|
|
327
|
+
const live_photo = Array.isArray(data.live_photo) ? data.live_photo.filter((lp) => lp && lp.image).map((lp) => ({
|
|
328
|
+
image: lp.image.startsWith('http') ? lp.image : 'https:' + lp.image,
|
|
329
|
+
video: lp.video ? (lp.video.startsWith('http') ? lp.video : 'https:' + lp.video) : ''
|
|
330
|
+
})) : [];
|
|
361
331
|
const music = {
|
|
362
332
|
title: data.music?.title || data.music?.name || '',
|
|
363
333
|
author: data.music?.author || data.music?.artist || '',
|
|
@@ -388,12 +358,7 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
388
358
|
else if (extra.create_time) {
|
|
389
359
|
publishTime = extra.create_time * 1000;
|
|
390
360
|
}
|
|
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
|
-
};
|
|
361
|
+
return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
|
|
397
362
|
}
|
|
398
363
|
const formatVarRegex = /\$\{([^}]+)\}/g;
|
|
399
364
|
function generateFormattedText(p, format) {
|
|
@@ -414,10 +379,6 @@ function generateFormattedText(p, format) {
|
|
|
414
379
|
'封面': p.cover,
|
|
415
380
|
'视频链接': p.video,
|
|
416
381
|
};
|
|
417
|
-
const varReplacements = Object.entries(vars).map(([key, val]) => ({
|
|
418
|
-
regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
|
|
419
|
-
value: val,
|
|
420
|
-
}));
|
|
421
382
|
const lines = format.split('\n');
|
|
422
383
|
const resultLines = [];
|
|
423
384
|
for (const line of lines) {
|
|
@@ -436,8 +397,8 @@ function generateFormattedText(p, format) {
|
|
|
436
397
|
continue;
|
|
437
398
|
}
|
|
438
399
|
let newLine = line;
|
|
439
|
-
for (const
|
|
440
|
-
newLine = newLine.replace(
|
|
400
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
401
|
+
newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
|
|
441
402
|
}
|
|
442
403
|
resultLines.push(newLine);
|
|
443
404
|
}
|
|
@@ -452,12 +413,7 @@ function buildForwardNode(session, content, botName) {
|
|
|
452
413
|
messageContent = [content];
|
|
453
414
|
else
|
|
454
415
|
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);
|
|
416
|
+
return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
|
|
461
417
|
}
|
|
462
418
|
function getErrorMessage(error) {
|
|
463
419
|
if (error instanceof Error)
|
|
@@ -468,7 +424,7 @@ function getErrorMessage(error) {
|
|
|
468
424
|
}
|
|
469
425
|
function apply(ctx, config) {
|
|
470
426
|
debugEnabled = config.debug || false;
|
|
471
|
-
debugLog('INFO', '
|
|
427
|
+
debugLog('INFO', 'plugin start');
|
|
472
428
|
const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
|
|
473
429
|
const texts = {
|
|
474
430
|
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
@@ -498,33 +454,47 @@ function apply(ctx, config) {
|
|
|
498
454
|
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
499
455
|
pipixia: 'https://api.bugpk.com/api/pipixia',
|
|
500
456
|
zuiyou: 'https://api.bugpk.com/api/zuiyou',
|
|
457
|
+
wechat_channel: 'https://api.bugpk.com/api/wxsph',
|
|
501
458
|
};
|
|
502
459
|
const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
|
|
503
460
|
function getPlatformConfig(type) {
|
|
504
461
|
const custom = config.customApis?.find((item) => item.platform === type);
|
|
505
462
|
let apiUrl = defaultDedicatedApis[type] || null;
|
|
463
|
+
let apiKey = '';
|
|
464
|
+
let authHeaderType = 'Bearer';
|
|
465
|
+
let customHeaderName = 'X-API-Key';
|
|
506
466
|
if (custom && custom.apiUrl) {
|
|
507
467
|
apiUrl = custom.apiUrl;
|
|
468
|
+
apiKey = custom.apiKey || '';
|
|
469
|
+
authHeaderType = custom.authHeaderType || 'Bearer';
|
|
470
|
+
customHeaderName = custom.customHeaderName || 'X-API-Key';
|
|
508
471
|
}
|
|
509
472
|
const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
|
|
510
|
-
return { apiUrl, dedicatedFirst };
|
|
473
|
+
return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName };
|
|
474
|
+
}
|
|
475
|
+
function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
|
|
476
|
+
if (!apiKey)
|
|
477
|
+
return {};
|
|
478
|
+
if (authHeaderType === 'Bearer')
|
|
479
|
+
return { 'Authorization': `Bearer ${apiKey}` };
|
|
480
|
+
if (authHeaderType === 'X-API-Key')
|
|
481
|
+
return { 'X-API-Key': apiKey };
|
|
482
|
+
if (authHeaderType === 'Custom' && customHeaderName)
|
|
483
|
+
return { [customHeaderName]: apiKey };
|
|
484
|
+
return {};
|
|
511
485
|
}
|
|
512
486
|
async function resolveShortUrl(url) {
|
|
513
487
|
try {
|
|
514
488
|
const res = await http.get(url, {
|
|
515
489
|
timeout: 10000,
|
|
516
490
|
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
|
-
},
|
|
491
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
|
|
521
492
|
validateStatus: (status) => status >= 200 && status < 400,
|
|
522
493
|
});
|
|
523
494
|
const finalUrl = res.request?.res?.responseUrl || url;
|
|
524
495
|
return cleanUrl(finalUrl);
|
|
525
496
|
}
|
|
526
|
-
catch
|
|
527
|
-
debugLog('WARN', '解析短链接失败:', e, '原始URL:', url);
|
|
497
|
+
catch {
|
|
528
498
|
return cleanUrl(url);
|
|
529
499
|
}
|
|
530
500
|
}
|
|
@@ -535,8 +505,6 @@ function apply(ctx, config) {
|
|
|
535
505
|
await promises_1.default.mkdir(tempDir, { recursive: true });
|
|
536
506
|
const fileName = `video_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.mp4`;
|
|
537
507
|
const filePath = path_1.default.resolve(tempDir, fileName);
|
|
538
|
-
debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
|
|
539
|
-
debugLog('INFO', `临时文件路径: ${filePath}`);
|
|
540
508
|
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
541
509
|
let response;
|
|
542
510
|
try {
|
|
@@ -545,10 +513,7 @@ function apply(ctx, config) {
|
|
|
545
513
|
url: videoUrl,
|
|
546
514
|
responseType: 'stream',
|
|
547
515
|
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
|
-
},
|
|
516
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
|
|
552
517
|
maxRedirects: 5,
|
|
553
518
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
554
519
|
});
|
|
@@ -567,7 +532,6 @@ function apply(ctx, config) {
|
|
|
567
532
|
}
|
|
568
533
|
try {
|
|
569
534
|
await (0, promises_2.pipeline)(response.data, writer);
|
|
570
|
-
debugLog('INFO', `视频下载完成`);
|
|
571
535
|
return filePath;
|
|
572
536
|
}
|
|
573
537
|
catch (e) {
|
|
@@ -578,17 +542,15 @@ function apply(ctx, config) {
|
|
|
578
542
|
async function fetchApi(url, type) {
|
|
579
543
|
const cacheKey = url;
|
|
580
544
|
const cached = urlCache.get(cacheKey);
|
|
581
|
-
if (cached && cached.expire > Date.now())
|
|
582
|
-
debugLog('DEBUG', `使用缓存: ${url}`);
|
|
545
|
+
if (cached && cached.expire > Date.now())
|
|
583
546
|
return cached.data;
|
|
584
|
-
}
|
|
585
|
-
const { apiUrl: dedicatedUrl, dedicatedFirst } = getPlatformConfig(type);
|
|
547
|
+
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
|
|
586
548
|
const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
|
|
587
549
|
const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
|
|
588
550
|
const backupAllowed = backupSupportedPlatforms.has(type);
|
|
589
551
|
const apiList = [];
|
|
590
552
|
if (dedicatedFirst && dedicatedUrl) {
|
|
591
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})
|
|
553
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
592
554
|
apiList.push({ url: primaryApi, label: '默认主API' });
|
|
593
555
|
if (backupAllowed)
|
|
594
556
|
apiList.push({ url: backupApi, label: '备用主API' });
|
|
@@ -598,16 +560,22 @@ function apply(ctx, config) {
|
|
|
598
560
|
if (backupAllowed)
|
|
599
561
|
apiList.push({ url: backupApi, label: '备用主API' });
|
|
600
562
|
if (dedicatedUrl)
|
|
601
|
-
apiList.push({ url: dedicatedUrl, label: `专属API(${type})
|
|
563
|
+
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName });
|
|
602
564
|
}
|
|
603
565
|
let lastError = null;
|
|
604
566
|
for (const api of apiList) {
|
|
605
567
|
for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
|
|
606
568
|
try {
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
569
|
+
const headers = {
|
|
570
|
+
'User-Agent': config.userAgent,
|
|
571
|
+
'Referer': 'https://www.baidu.com/',
|
|
572
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
573
|
+
};
|
|
574
|
+
if (api.apiKey) {
|
|
575
|
+
const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
|
|
576
|
+
Object.assign(headers, authHeaders);
|
|
577
|
+
}
|
|
578
|
+
const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
|
|
611
579
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
612
580
|
const parsed = parseApiResponse(res.data, config.maxDescLength);
|
|
613
581
|
urlCache.set(cacheKey, { data: parsed, expire: Date.now() + 10 * 60 * 1000 });
|
|
@@ -617,81 +585,59 @@ function apply(ctx, config) {
|
|
|
617
585
|
}
|
|
618
586
|
catch (error) {
|
|
619
587
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
620
|
-
debugLog('ERROR', `${api.label}
|
|
621
|
-
if (attempt < config.retryTimes)
|
|
588
|
+
debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
|
|
589
|
+
if (attempt < config.retryTimes)
|
|
622
590
|
await delay(config.retryInterval);
|
|
623
|
-
}
|
|
624
591
|
}
|
|
625
592
|
}
|
|
626
|
-
debugLog('WARN', `${api.label}
|
|
593
|
+
debugLog('WARN', `${api.label} all retries failed`);
|
|
627
594
|
}
|
|
628
595
|
throw lastError || new Error('所有API请求全部失败');
|
|
629
596
|
}
|
|
630
597
|
async function parseUrl(url, type) {
|
|
631
598
|
const realUrl = await resolveShortUrl(url);
|
|
632
|
-
const candidates = [realUrl, url];
|
|
633
|
-
for (const candidate of
|
|
599
|
+
const candidates = [...new Set([realUrl, url])];
|
|
600
|
+
for (const candidate of candidates) {
|
|
634
601
|
try {
|
|
635
602
|
const info = await fetchApi(candidate, type);
|
|
636
|
-
if (info.video || info.images.length > 0)
|
|
603
|
+
if (info.video || info.images.length > 0)
|
|
637
604
|
return { success: true, data: info };
|
|
638
|
-
}
|
|
639
|
-
debugLog('WARN', `解析成功但无有效内容: ${candidate}`);
|
|
605
|
+
debugLog('WARN', `解析成功但无内容: ${candidate}`);
|
|
640
606
|
}
|
|
641
607
|
catch (error) {
|
|
642
|
-
debugLog('ERROR',
|
|
608
|
+
debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
|
|
643
609
|
}
|
|
644
610
|
}
|
|
645
611
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
646
612
|
}
|
|
647
613
|
async function processSingleUrl(url, type) {
|
|
648
614
|
const result = await parseUrl(url, type);
|
|
649
|
-
if (!result.success)
|
|
615
|
+
if (!result.success)
|
|
650
616
|
return { success: false, msg: result.msg, url };
|
|
651
|
-
}
|
|
652
617
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
653
|
-
return {
|
|
654
|
-
success: true,
|
|
655
|
-
data: {
|
|
656
|
-
text,
|
|
657
|
-
parsed: result.data
|
|
658
|
-
}
|
|
659
|
-
};
|
|
618
|
+
return { success: true, data: { text, parsed: result.data } };
|
|
660
619
|
}
|
|
661
620
|
async function sendWithTimeout(session, content, customRetries) {
|
|
662
621
|
const maxRetries = customRetries ?? config.retryTimes ?? 3;
|
|
663
622
|
const retryDelay = config.retryInterval || 1000;
|
|
664
|
-
let timeoutId = null;
|
|
665
623
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
666
624
|
try {
|
|
667
625
|
let sendPromise = session.send(content);
|
|
668
626
|
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;
|
|
627
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout));
|
|
628
|
+
return await Promise.race([sendPromise, timeoutPromise]);
|
|
676
629
|
}
|
|
677
630
|
else {
|
|
678
631
|
return await sendPromise;
|
|
679
632
|
}
|
|
680
633
|
}
|
|
681
634
|
catch (err) {
|
|
682
|
-
if (timeoutId)
|
|
683
|
-
clearTimeout(timeoutId);
|
|
684
635
|
const errMsg = getErrorMessage(err);
|
|
685
|
-
debugLog('ERROR',
|
|
686
|
-
if (attempt < maxRetries)
|
|
687
|
-
debugLog('INFO', `等待 ${retryDelay}ms 后进行第 ${attempt + 2} 次重试`);
|
|
636
|
+
debugLog('ERROR', `发送失败尝试 ${attempt + 1}: ${errMsg}`);
|
|
637
|
+
if (attempt < maxRetries)
|
|
688
638
|
await delay(retryDelay);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (!config.ignoreSendError)
|
|
692
|
-
throw err;
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
639
|
+
else if (!config.ignoreSendError)
|
|
640
|
+
throw err;
|
|
695
641
|
}
|
|
696
642
|
}
|
|
697
643
|
return null;
|
|
@@ -699,47 +645,36 @@ function apply(ctx, config) {
|
|
|
699
645
|
async function sendVideoFile(session, videoUrl) {
|
|
700
646
|
if (!videoUrl)
|
|
701
647
|
return;
|
|
702
|
-
if (!config.showVideoFile)
|
|
648
|
+
if (!config.showVideoFile)
|
|
703
649
|
return await sendWithTimeout(session, `视频链接:${videoUrl}`);
|
|
704
|
-
}
|
|
705
|
-
const sendLink = async () => {
|
|
706
|
-
await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { });
|
|
707
|
-
};
|
|
650
|
+
const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
|
|
708
651
|
if (config.forceDownloadVideo) {
|
|
709
652
|
try {
|
|
710
653
|
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
711
|
-
|
|
712
|
-
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
654
|
+
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
713
655
|
return;
|
|
714
656
|
}
|
|
715
657
|
catch (e) {
|
|
716
|
-
debugLog('ERROR', '
|
|
658
|
+
debugLog('ERROR', '强制下载失败,尝试URL发送:', getErrorMessage(e));
|
|
717
659
|
try {
|
|
718
660
|
await sendWithTimeout(session, koishi_1.h.video(videoUrl));
|
|
719
661
|
return;
|
|
720
662
|
}
|
|
721
|
-
catch
|
|
722
|
-
debugLog('ERROR', '发送URL也失败,降级发送链接:', getErrorMessage(urlErr));
|
|
663
|
+
catch {
|
|
723
664
|
await sendLink();
|
|
724
665
|
}
|
|
725
666
|
}
|
|
726
667
|
return;
|
|
727
668
|
}
|
|
728
669
|
try {
|
|
729
|
-
debugLog('INFO', '尝试直接发送视频URL');
|
|
730
670
|
await sendWithTimeout(session, koishi_1.h.video(videoUrl));
|
|
731
|
-
return;
|
|
732
671
|
}
|
|
733
|
-
catch
|
|
734
|
-
debugLog('ERROR', '直接发送URL失败,尝试下载:', getErrorMessage(urlErr));
|
|
672
|
+
catch {
|
|
735
673
|
try {
|
|
736
674
|
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
737
|
-
|
|
738
|
-
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
739
|
-
return;
|
|
675
|
+
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
740
676
|
}
|
|
741
|
-
catch
|
|
742
|
-
debugLog('ERROR', '下载失败,降级发送链接:', getErrorMessage(downloadErr));
|
|
677
|
+
catch {
|
|
743
678
|
await sendLink();
|
|
744
679
|
}
|
|
745
680
|
}
|
|
@@ -755,37 +690,28 @@ function apply(ctx, config) {
|
|
|
755
690
|
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
756
691
|
debugLog('INFO', `跳过重复链接: ${match.url}`);
|
|
757
692
|
const shortUrl = match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url;
|
|
758
|
-
|
|
759
|
-
await sendWithTimeout(session, skipMsg).catch(() => { });
|
|
693
|
+
await sendWithTimeout(session, `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`).catch(() => { });
|
|
760
694
|
continue;
|
|
761
695
|
}
|
|
762
696
|
}
|
|
763
|
-
debugLog('INFO',
|
|
697
|
+
debugLog('INFO', `解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (${match.type})`);
|
|
764
698
|
const result = await processSingleUrl(match.url, match.type);
|
|
765
699
|
if (result.success) {
|
|
766
700
|
items.push(result.data);
|
|
767
|
-
if (config.deduplicationInterval > 0)
|
|
701
|
+
if (config.deduplicationInterval > 0)
|
|
768
702
|
dedupCache.set(match.url, Date.now());
|
|
769
|
-
}
|
|
770
703
|
}
|
|
771
704
|
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);
|
|
705
|
+
const item = texts.parseErrorItemFormat.replace(/\$\{url\}/g, match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url).replace(/\$\{msg\}/g, result.msg);
|
|
775
706
|
errors.push(item);
|
|
776
707
|
}
|
|
777
|
-
if (i < matches.length - 1)
|
|
708
|
+
if (i < matches.length - 1)
|
|
778
709
|
await delay(500);
|
|
779
|
-
}
|
|
780
710
|
}
|
|
781
|
-
if (errors.length)
|
|
711
|
+
if (errors.length)
|
|
782
712
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
if (!items.length) {
|
|
786
|
-
debugLog('INFO', '没有成功解析的内容');
|
|
713
|
+
if (!items.length)
|
|
787
714
|
return;
|
|
788
|
-
}
|
|
789
715
|
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
790
716
|
const botName = config.botName || '视频解析机器人';
|
|
791
717
|
if (enableForward) {
|
|
@@ -793,30 +719,24 @@ function apply(ctx, config) {
|
|
|
793
719
|
for (const item of items) {
|
|
794
720
|
const p = item.parsed;
|
|
795
721
|
const text = item.text;
|
|
796
|
-
if (text && config.showImageText)
|
|
722
|
+
if (text && config.showImageText)
|
|
797
723
|
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))) {
|
|
724
|
+
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
|
|
800
725
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
801
|
-
}
|
|
802
726
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
803
727
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
804
|
-
for (const imgUrl of imageUrls)
|
|
728
|
+
for (const imgUrl of imageUrls)
|
|
805
729
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
|
|
806
|
-
}
|
|
807
730
|
}
|
|
808
|
-
if (p.video)
|
|
731
|
+
if (p.video)
|
|
809
732
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
|
|
810
|
-
}
|
|
811
733
|
}
|
|
812
734
|
if (forwardMessages.length) {
|
|
813
|
-
const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100));
|
|
814
735
|
try {
|
|
815
|
-
|
|
816
|
-
await sendWithTimeout(session, forwardMsg, config.retryTimes);
|
|
736
|
+
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)), config.retryTimes);
|
|
817
737
|
}
|
|
818
738
|
catch (err) {
|
|
819
|
-
debugLog('ERROR', '
|
|
739
|
+
debugLog('ERROR', '合并转发失败,降级逐条发送:', err);
|
|
820
740
|
for (const node of forwardMessages) {
|
|
821
741
|
await sendWithTimeout(session, node.data.content).catch(() => { });
|
|
822
742
|
await delay(300);
|
|
@@ -837,17 +757,10 @@ function apply(ctx, config) {
|
|
|
837
757
|
await delay(300);
|
|
838
758
|
}
|
|
839
759
|
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 {
|
|
760
|
+
if (config.showVideoFile)
|
|
761
|
+
await sendVideoFile(session, p.video);
|
|
762
|
+
else
|
|
849
763
|
await sendWithTimeout(session, `视频链接:${p.video}`);
|
|
850
|
-
}
|
|
851
764
|
await delay(500);
|
|
852
765
|
}
|
|
853
766
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
@@ -859,7 +772,7 @@ function apply(ctx, config) {
|
|
|
859
772
|
}
|
|
860
773
|
}
|
|
861
774
|
}
|
|
862
|
-
debugLog('INFO', '
|
|
775
|
+
debugLog('INFO', '处理完成');
|
|
863
776
|
}
|
|
864
777
|
ctx.on('message', async (session) => {
|
|
865
778
|
if (!config.enable)
|
|
@@ -873,13 +786,13 @@ function apply(ctx, config) {
|
|
|
873
786
|
const matches = extractAllUrlsFromMessage(session);
|
|
874
787
|
if (!matches.length)
|
|
875
788
|
return;
|
|
876
|
-
debugLog('INFO', `检测到 ${matches.length}
|
|
789
|
+
debugLog('INFO', `检测到 ${matches.length} 个链接`);
|
|
877
790
|
if (config.showWaitingTip) {
|
|
878
791
|
try {
|
|
879
792
|
await sendWithTimeout(session, texts.waitingTipText);
|
|
880
793
|
}
|
|
881
794
|
catch (e) {
|
|
882
|
-
debugLog('WARN', '
|
|
795
|
+
debugLog('WARN', '等待提示发送失败:', e);
|
|
883
796
|
}
|
|
884
797
|
}
|
|
885
798
|
await flush(session, matches);
|
|
@@ -907,20 +820,19 @@ function apply(ctx, config) {
|
|
|
907
820
|
const tempDir = config.tempDir || './temp_videos';
|
|
908
821
|
const files = await promises_1.default.readdir(tempDir);
|
|
909
822
|
const now = Date.now();
|
|
910
|
-
let
|
|
823
|
+
let deleted = 0;
|
|
911
824
|
for (const file of files) {
|
|
912
825
|
if (file.startsWith('video_') && file.endsWith('.mp4')) {
|
|
913
826
|
const filePath = path_1.default.join(tempDir, file);
|
|
914
827
|
const stats = await promises_1.default.stat(filePath);
|
|
915
828
|
if (now - stats.mtimeMs > 3600000) {
|
|
916
829
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
917
|
-
|
|
830
|
+
deleted++;
|
|
918
831
|
}
|
|
919
832
|
}
|
|
920
833
|
}
|
|
921
|
-
if (
|
|
922
|
-
debugLog('INFO', `清理了 ${
|
|
923
|
-
}
|
|
834
|
+
if (deleted)
|
|
835
|
+
debugLog('INFO', `清理了 ${deleted} 个过期临时视频文件`);
|
|
924
836
|
}
|
|
925
837
|
catch (e) {
|
|
926
838
|
debugLog('WARN', '清理临时文件失败:', e);
|
|
@@ -930,18 +842,16 @@ function apply(ctx, config) {
|
|
|
930
842
|
clearInterval(tempCleanupInterval);
|
|
931
843
|
urlCache.clear();
|
|
932
844
|
dedupCache.clear();
|
|
933
|
-
debugLog('INFO', '
|
|
845
|
+
debugLog('INFO', '插件已卸载');
|
|
934
846
|
});
|
|
935
847
|
process.on('beforeExit', async () => {
|
|
936
848
|
try {
|
|
937
849
|
const tempDir = config.tempDir || './temp_videos';
|
|
938
850
|
const files = await promises_1.default.readdir(tempDir);
|
|
939
851
|
for (const file of files) {
|
|
940
|
-
if (file.startsWith('video_') && file.endsWith('.mp4'))
|
|
852
|
+
if (file.startsWith('video_') && file.endsWith('.mp4'))
|
|
941
853
|
await promises_1.default.unlink(path_1.default.join(tempDir, file)).catch(() => { });
|
|
942
|
-
}
|
|
943
854
|
}
|
|
944
|
-
debugLog('INFO', '进程退出,已清理所有临时视频文件');
|
|
945
855
|
}
|
|
946
856
|
catch { }
|
|
947
857
|
});
|
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
|
|