koishi-plugin-video-parser-all 1.3.4 → 1.3.5
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 +113 -0
- package/lib/index.js +253 -222
- package/package.json +14 -2
- package/readme.md +25 -12
package/lib/index.d.ts
CHANGED
|
@@ -5,6 +5,28 @@ export declare const Config: Schema<{
|
|
|
5
5
|
botName?: string | null | undefined;
|
|
6
6
|
showWaitingTip?: boolean | null | undefined;
|
|
7
7
|
debug?: boolean | null | undefined;
|
|
8
|
+
platformEnabled?: ({
|
|
9
|
+
bilibili?: boolean | null | undefined;
|
|
10
|
+
douyin?: boolean | null | undefined;
|
|
11
|
+
kuaishou?: boolean | null | undefined;
|
|
12
|
+
xiaohongshu?: boolean | null | undefined;
|
|
13
|
+
weibo?: boolean | null | undefined;
|
|
14
|
+
xigua?: boolean | null | undefined;
|
|
15
|
+
youtube?: boolean | null | undefined;
|
|
16
|
+
tiktok?: boolean | null | undefined;
|
|
17
|
+
acfun?: boolean | null | undefined;
|
|
18
|
+
zhihu?: boolean | null | undefined;
|
|
19
|
+
weishi?: boolean | null | undefined;
|
|
20
|
+
huya?: boolean | null | undefined;
|
|
21
|
+
haokan?: boolean | null | undefined;
|
|
22
|
+
meipai?: boolean | null | undefined;
|
|
23
|
+
twitter?: boolean | null | undefined;
|
|
24
|
+
instagram?: boolean | null | undefined;
|
|
25
|
+
doubao?: boolean | null | undefined;
|
|
26
|
+
doubao_chat?: boolean | null | undefined;
|
|
27
|
+
oasis?: boolean | null | undefined;
|
|
28
|
+
wechat_channel?: boolean | null | undefined;
|
|
29
|
+
} & import("cosmokit").Dict) | null | undefined;
|
|
8
30
|
} & import("cosmokit").Dict & {
|
|
9
31
|
unifiedMessageFormat?: string | null | undefined;
|
|
10
32
|
} & {
|
|
@@ -23,6 +45,13 @@ export declare const Config: Schema<{
|
|
|
23
45
|
maxVideoSize?: number | null | undefined;
|
|
24
46
|
maxDescLength?: number | null | undefined;
|
|
25
47
|
maxConcurrent?: number | null | undefined;
|
|
48
|
+
downloadConcurrency?: number | null | undefined;
|
|
49
|
+
showMusicVoice?: boolean | null | undefined;
|
|
50
|
+
showMusicVoiceFile?: boolean | null | undefined;
|
|
51
|
+
forceDownloadMusicVoice?: boolean | null | undefined;
|
|
52
|
+
musicDownloadTimeout?: number | null | undefined;
|
|
53
|
+
musicTempDir?: string | null | undefined;
|
|
54
|
+
maxMusicSize?: number | null | undefined;
|
|
26
55
|
} & {
|
|
27
56
|
timeout?: number | null | undefined;
|
|
28
57
|
videoSendTimeout?: number | null | undefined;
|
|
@@ -83,6 +112,26 @@ export declare const Config: Schema<{
|
|
|
83
112
|
customHeaderName?: string | null | undefined;
|
|
84
113
|
fieldMapping?: string | null | undefined;
|
|
85
114
|
} & import("cosmokit").Dict)[] | null | undefined;
|
|
115
|
+
customPlatforms?: ({
|
|
116
|
+
name?: string | null | undefined;
|
|
117
|
+
exampleUrl?: string | null | undefined;
|
|
118
|
+
keywords?: string | null | undefined;
|
|
119
|
+
apiUrl?: string | null | undefined;
|
|
120
|
+
apiKey?: string | null | undefined;
|
|
121
|
+
authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
|
|
122
|
+
customHeaderName?: string | null | undefined;
|
|
123
|
+
fieldMapping?: string | null | undefined;
|
|
124
|
+
proxy?: ({
|
|
125
|
+
enabled?: boolean | null | undefined;
|
|
126
|
+
protocol?: "http" | "https" | null | undefined;
|
|
127
|
+
host?: string | null | undefined;
|
|
128
|
+
port?: number | null | undefined;
|
|
129
|
+
auth?: ({
|
|
130
|
+
username?: string | null | undefined;
|
|
131
|
+
password?: string | null | undefined;
|
|
132
|
+
} & import("cosmokit").Dict) | null | undefined;
|
|
133
|
+
} & import("cosmokit").Dict) | null | undefined;
|
|
134
|
+
} & import("cosmokit").Dict)[] | null | undefined;
|
|
86
135
|
globalFieldMapping?: string | null | undefined;
|
|
87
136
|
} & {
|
|
88
137
|
waitingTipText?: string | null | undefined;
|
|
@@ -95,6 +144,28 @@ export declare const Config: Schema<{
|
|
|
95
144
|
botName: string;
|
|
96
145
|
showWaitingTip: boolean;
|
|
97
146
|
debug: boolean;
|
|
147
|
+
platformEnabled: Schemastery.ObjectT<{
|
|
148
|
+
bilibili: Schema<boolean, boolean>;
|
|
149
|
+
douyin: Schema<boolean, boolean>;
|
|
150
|
+
kuaishou: Schema<boolean, boolean>;
|
|
151
|
+
xiaohongshu: Schema<boolean, boolean>;
|
|
152
|
+
weibo: Schema<boolean, boolean>;
|
|
153
|
+
xigua: Schema<boolean, boolean>;
|
|
154
|
+
youtube: Schema<boolean, boolean>;
|
|
155
|
+
tiktok: Schema<boolean, boolean>;
|
|
156
|
+
acfun: Schema<boolean, boolean>;
|
|
157
|
+
zhihu: Schema<boolean, boolean>;
|
|
158
|
+
weishi: Schema<boolean, boolean>;
|
|
159
|
+
huya: Schema<boolean, boolean>;
|
|
160
|
+
haokan: Schema<boolean, boolean>;
|
|
161
|
+
meipai: Schema<boolean, boolean>;
|
|
162
|
+
twitter: Schema<boolean, boolean>;
|
|
163
|
+
instagram: Schema<boolean, boolean>;
|
|
164
|
+
doubao: Schema<boolean, boolean>;
|
|
165
|
+
doubao_chat: Schema<boolean, boolean>;
|
|
166
|
+
oasis: Schema<boolean, boolean>;
|
|
167
|
+
wechat_channel: Schema<boolean, boolean>;
|
|
168
|
+
}>;
|
|
98
169
|
} & import("cosmokit").Dict & {
|
|
99
170
|
unifiedMessageFormat: string;
|
|
100
171
|
} & {
|
|
@@ -113,6 +184,13 @@ export declare const Config: Schema<{
|
|
|
113
184
|
maxVideoSize: number;
|
|
114
185
|
maxDescLength: number;
|
|
115
186
|
maxConcurrent: number;
|
|
187
|
+
downloadConcurrency: number;
|
|
188
|
+
showMusicVoice: boolean;
|
|
189
|
+
showMusicVoiceFile: boolean;
|
|
190
|
+
forceDownloadMusicVoice: boolean;
|
|
191
|
+
musicDownloadTimeout: number;
|
|
192
|
+
musicTempDir: string;
|
|
193
|
+
maxMusicSize: number;
|
|
116
194
|
} & {
|
|
117
195
|
timeout: number;
|
|
118
196
|
videoSendTimeout: number;
|
|
@@ -176,6 +254,41 @@ export declare const Config: Schema<{
|
|
|
176
254
|
customHeaderName: Schema<string, string>;
|
|
177
255
|
fieldMapping: Schema<string, string>;
|
|
178
256
|
}>[];
|
|
257
|
+
customPlatforms: Schemastery.ObjectT<{
|
|
258
|
+
name: Schema<string, string>;
|
|
259
|
+
exampleUrl: Schema<string, string>;
|
|
260
|
+
keywords: Schema<string, string>;
|
|
261
|
+
apiUrl: Schema<string, string>;
|
|
262
|
+
apiKey: Schema<string, string>;
|
|
263
|
+
authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
|
|
264
|
+
customHeaderName: Schema<string, string>;
|
|
265
|
+
fieldMapping: Schema<string, string>;
|
|
266
|
+
proxy: Schema<Schemastery.ObjectS<{
|
|
267
|
+
enabled: Schema<boolean, boolean>;
|
|
268
|
+
protocol: Schema<"http" | "https", "http" | "https">;
|
|
269
|
+
host: Schema<string, string>;
|
|
270
|
+
port: Schema<number, number>;
|
|
271
|
+
auth: Schema<Schemastery.ObjectS<{
|
|
272
|
+
username: Schema<string, string>;
|
|
273
|
+
password: Schema<string, string>;
|
|
274
|
+
}>, Schemastery.ObjectT<{
|
|
275
|
+
username: Schema<string, string>;
|
|
276
|
+
password: Schema<string, string>;
|
|
277
|
+
}>>;
|
|
278
|
+
}>, Schemastery.ObjectT<{
|
|
279
|
+
enabled: Schema<boolean, boolean>;
|
|
280
|
+
protocol: Schema<"http" | "https", "http" | "https">;
|
|
281
|
+
host: Schema<string, string>;
|
|
282
|
+
port: Schema<number, number>;
|
|
283
|
+
auth: Schema<Schemastery.ObjectS<{
|
|
284
|
+
username: Schema<string, string>;
|
|
285
|
+
password: Schema<string, string>;
|
|
286
|
+
}>, Schemastery.ObjectT<{
|
|
287
|
+
username: Schema<string, string>;
|
|
288
|
+
password: Schema<string, string>;
|
|
289
|
+
}>>;
|
|
290
|
+
}>>;
|
|
291
|
+
}>[];
|
|
179
292
|
globalFieldMapping: string;
|
|
180
293
|
} & {
|
|
181
294
|
waitingTipText: string;
|
package/lib/index.js
CHANGED
|
@@ -74,9 +74,31 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
74
74
|
botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发消息中显示的机器人名称'),
|
|
75
75
|
showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
|
|
76
76
|
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
|
|
77
|
+
platformEnabled: koishi_1.Schema.object({
|
|
78
|
+
bilibili: koishi_1.Schema.boolean().default(true).description('哔哩哔哩'),
|
|
79
|
+
douyin: koishi_1.Schema.boolean().default(true).description('抖音'),
|
|
80
|
+
kuaishou: koishi_1.Schema.boolean().default(true).description('快手'),
|
|
81
|
+
xiaohongshu: koishi_1.Schema.boolean().default(true).description('小红书'),
|
|
82
|
+
weibo: koishi_1.Schema.boolean().default(true).description('微博'),
|
|
83
|
+
xigua: koishi_1.Schema.boolean().default(true).description('西瓜视频'),
|
|
84
|
+
youtube: koishi_1.Schema.boolean().default(true).description('YouTube'),
|
|
85
|
+
tiktok: koishi_1.Schema.boolean().default(true).description('TikTok'),
|
|
86
|
+
acfun: koishi_1.Schema.boolean().default(true).description('AcFun(A站)'),
|
|
87
|
+
zhihu: koishi_1.Schema.boolean().default(true).description('知乎'),
|
|
88
|
+
weishi: koishi_1.Schema.boolean().default(true).description('微视'),
|
|
89
|
+
huya: koishi_1.Schema.boolean().default(true).description('虎牙'),
|
|
90
|
+
haokan: koishi_1.Schema.boolean().default(true).description('好看视频'),
|
|
91
|
+
meipai: koishi_1.Schema.boolean().default(true).description('美拍'),
|
|
92
|
+
twitter: koishi_1.Schema.boolean().default(true).description('Twitter/X'),
|
|
93
|
+
instagram: koishi_1.Schema.boolean().default(true).description('Instagram'),
|
|
94
|
+
doubao: koishi_1.Schema.boolean().default(true).description('豆包'),
|
|
95
|
+
doubao_chat: koishi_1.Schema.boolean().default(true).description('豆包对话'),
|
|
96
|
+
oasis: koishi_1.Schema.boolean().default(true).description('绿洲'),
|
|
97
|
+
wechat_channel: koishi_1.Schema.boolean().default(true).description('视频号'),
|
|
98
|
+
}).description('各平台解析开关'),
|
|
77
99
|
}).description('基础设置'),
|
|
78
100
|
koishi_1.Schema.object({
|
|
79
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n
|
|
101
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('文字消息格式,支持变量。某行所有变量为空时自动隐藏。封面及媒体文件由独立开关控制,默认不包含在文字中'),
|
|
80
102
|
}).description('消息格式设置'),
|
|
81
103
|
koishi_1.Schema.object({
|
|
82
104
|
showImageText: koishi_1.Schema.boolean().default(true).description('是否发送文字内容'),
|
|
@@ -93,7 +115,14 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
93
115
|
tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
|
|
94
116
|
maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
|
|
95
117
|
maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介最大长度(字符)'),
|
|
96
|
-
maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('
|
|
118
|
+
maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('解析最大并发数'),
|
|
119
|
+
downloadConcurrency: koishi_1.Schema.number().min(1).step(1).default(3).description('下载线程数'),
|
|
120
|
+
showMusicVoice: koishi_1.Schema.boolean().default(false).description('是否发送音乐(转语音)'),
|
|
121
|
+
showMusicVoiceFile: koishi_1.Schema.boolean().default(true).description('音乐语音是否以文件形式发送(关闭则只发送链接)'),
|
|
122
|
+
forceDownloadMusicVoice: koishi_1.Schema.boolean().default(false).description('强制下载音乐语音后发送'),
|
|
123
|
+
musicDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('音乐下载超时(毫秒)'),
|
|
124
|
+
musicTempDir: koishi_1.Schema.string().default('./temp_music').description('临时音乐存储目录'),
|
|
125
|
+
maxMusicSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载音乐大小(MB),0 为不限制'),
|
|
97
126
|
}).description('内容显示设置'),
|
|
98
127
|
koishi_1.Schema.object({
|
|
99
128
|
timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
|
|
@@ -123,15 +152,15 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
123
152
|
retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
|
|
124
153
|
}).description('错误与重试'),
|
|
125
154
|
koishi_1.Schema.object({
|
|
126
|
-
enableForward: koishi_1.Schema.boolean().default(false).description('
|
|
155
|
+
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(支持 OneBot、Satori 平台)'),
|
|
127
156
|
}).description('发送方式'),
|
|
128
157
|
koishi_1.Schema.object({
|
|
129
158
|
deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
|
|
130
159
|
cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
|
|
131
160
|
}).description('缓存与去重'),
|
|
132
161
|
koishi_1.Schema.object({
|
|
133
|
-
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').
|
|
134
|
-
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').
|
|
162
|
+
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').hidden(),
|
|
163
|
+
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').hidden(),
|
|
135
164
|
platformDedicatedFirst: koishi_1.Schema.object({
|
|
136
165
|
bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
|
|
137
166
|
douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
|
|
@@ -141,7 +170,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
141
170
|
xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
|
|
142
171
|
youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
|
|
143
172
|
tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
|
|
144
|
-
acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
|
|
173
|
+
acfun: koishi_1.Schema.boolean().default(false).description('AcFun(A站)'),
|
|
145
174
|
zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
|
|
146
175
|
weishi: koishi_1.Schema.boolean().default(false).description('微视'),
|
|
147
176
|
huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
|
|
@@ -164,7 +193,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
164
193
|
koishi_1.Schema.const('xigua').description('西瓜视频'),
|
|
165
194
|
koishi_1.Schema.const('youtube').description('YouTube'),
|
|
166
195
|
koishi_1.Schema.const('tiktok').description('TikTok'),
|
|
167
|
-
koishi_1.Schema.const('acfun').description('AcFun'),
|
|
196
|
+
koishi_1.Schema.const('acfun').description('AcFun(A站)'),
|
|
168
197
|
koishi_1.Schema.const('zhihu').description('知乎'),
|
|
169
198
|
koishi_1.Schema.const('weishi').description('微视'),
|
|
170
199
|
koishi_1.Schema.const('huya').description('虎牙'),
|
|
@@ -186,7 +215,34 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
186
215
|
]).default('Bearer').description('认证头类型'),
|
|
187
216
|
customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
|
|
188
217
|
fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
|
|
189
|
-
})).default([]).description('
|
|
218
|
+
})).default([]).description('自定义内置平台专属 API'),
|
|
219
|
+
customPlatforms: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
220
|
+
name: koishi_1.Schema.string().required().description('平台名称'),
|
|
221
|
+
exampleUrl: koishi_1.Schema.string().description('示例视频链接'),
|
|
222
|
+
keywords: koishi_1.Schema.string().required().description('匹配关键词,逗号分隔'),
|
|
223
|
+
apiUrl: koishi_1.Schema.string().required().description('解析 API 地址'),
|
|
224
|
+
apiKey: koishi_1.Schema.string().default('').description('API Key'),
|
|
225
|
+
authHeaderType: koishi_1.Schema.union([
|
|
226
|
+
koishi_1.Schema.const('Bearer').description('Bearer'),
|
|
227
|
+
koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
|
|
228
|
+
koishi_1.Schema.const('Custom').description('自定义'),
|
|
229
|
+
]).default('Bearer').description('认证头类型'),
|
|
230
|
+
customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
|
|
231
|
+
fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
|
|
232
|
+
proxy: koishi_1.Schema.object({
|
|
233
|
+
enabled: koishi_1.Schema.boolean().default(false).description('启用独立代理'),
|
|
234
|
+
protocol: koishi_1.Schema.union([
|
|
235
|
+
koishi_1.Schema.const('http').description('HTTP'),
|
|
236
|
+
koishi_1.Schema.const('https').description('HTTPS'),
|
|
237
|
+
]).default('http').description('协议'),
|
|
238
|
+
host: koishi_1.Schema.string().default('127.0.0.1').description('地址'),
|
|
239
|
+
port: koishi_1.Schema.number().default(7890).description('端口'),
|
|
240
|
+
auth: koishi_1.Schema.object({
|
|
241
|
+
username: koishi_1.Schema.string().default('').description('用户名'),
|
|
242
|
+
password: koishi_1.Schema.string().default('').description('密码'),
|
|
243
|
+
}).description('认证'),
|
|
244
|
+
}).description('独立代理(覆盖全局代理)'),
|
|
245
|
+
})).default([]).description('自定义新平台'),
|
|
190
246
|
globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
|
|
191
247
|
' "title": "data.title",\n' +
|
|
192
248
|
' "desc": "data.description",\n' +
|
|
@@ -226,7 +282,7 @@ function debugLog(level, ...args) {
|
|
|
226
282
|
return;
|
|
227
283
|
logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
|
|
228
284
|
}
|
|
229
|
-
const
|
|
285
|
+
const BUILTIN_LINK_RULES = [
|
|
230
286
|
{ pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
|
|
231
287
|
{ pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
|
|
232
288
|
{ pattern: /https?:\/\/bili\d+\.cn\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
|
|
@@ -258,11 +314,27 @@ const LINK_RULES = [
|
|
|
258
314
|
{ pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
259
315
|
{ pattern: /https?:\/\/weixin\.qq\.com\/sph\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
260
316
|
];
|
|
261
|
-
function
|
|
317
|
+
function buildCustomLinkRules(customPlatforms) {
|
|
318
|
+
if (!Array.isArray(customPlatforms) || customPlatforms.length === 0)
|
|
319
|
+
return [];
|
|
320
|
+
return customPlatforms
|
|
321
|
+
.filter(p => p.keywords)
|
|
322
|
+
.map(p => {
|
|
323
|
+
const keywords = p.keywords.split(',').map((s) => s.trim()).filter(Boolean);
|
|
324
|
+
if (keywords.length === 0)
|
|
325
|
+
return null;
|
|
326
|
+
const escaped = keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
327
|
+
const pattern = new RegExp('https?://[^/\\s]*(' + escaped.join('|') + ')[^\\s]*', 'gi');
|
|
328
|
+
return { pattern, type: `custom_${p.name}` };
|
|
329
|
+
})
|
|
330
|
+
.filter(Boolean);
|
|
331
|
+
}
|
|
332
|
+
function linkTypeParser(content, customRules) {
|
|
262
333
|
content = content.replace(/\\\//g, '/');
|
|
334
|
+
const allRules = [...BUILTIN_LINK_RULES, ...customRules];
|
|
263
335
|
const matches = [];
|
|
264
336
|
const seen = new Set();
|
|
265
|
-
for (const rule of
|
|
337
|
+
for (const rule of allRules) {
|
|
266
338
|
let match;
|
|
267
339
|
rule.pattern.lastIndex = 0;
|
|
268
340
|
while ((match = rule.pattern.exec(content)) !== null) {
|
|
@@ -275,9 +347,9 @@ function linkTypeParser(content) {
|
|
|
275
347
|
}
|
|
276
348
|
return matches;
|
|
277
349
|
}
|
|
278
|
-
function extractAllUrlsFromMessage(session) {
|
|
350
|
+
function extractAllUrlsFromMessage(session, customRules) {
|
|
279
351
|
const content = session.content?.trim() || '';
|
|
280
|
-
const matchedLinks = linkTypeParser(content);
|
|
352
|
+
const matchedLinks = linkTypeParser(content, customRules);
|
|
281
353
|
const cardsContent = [];
|
|
282
354
|
if (session.elements) {
|
|
283
355
|
for (const elem of session.elements) {
|
|
@@ -303,7 +375,7 @@ function extractAllUrlsFromMessage(session) {
|
|
|
303
375
|
}
|
|
304
376
|
}
|
|
305
377
|
for (const cardContent of cardsContent) {
|
|
306
|
-
matchedLinks.push(...linkTypeParser(cardContent));
|
|
378
|
+
matchedLinks.push(...linkTypeParser(cardContent, customRules));
|
|
307
379
|
}
|
|
308
380
|
const seen = new Set();
|
|
309
381
|
const result = [];
|
|
@@ -497,8 +569,6 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
|
|
|
497
569
|
const durRaw = mapField('duration', () => data.duration);
|
|
498
570
|
if (durRaw) {
|
|
499
571
|
duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
|
|
500
|
-
if (duration > 3600)
|
|
501
|
-
duration = Math.floor(duration / 1000);
|
|
502
572
|
}
|
|
503
573
|
}
|
|
504
574
|
let publishTime = 0;
|
|
@@ -533,7 +603,6 @@ function generateFormattedText(p, format) {
|
|
|
533
603
|
'音乐标题': p.music.title || '',
|
|
534
604
|
'音乐作者': p.music.author || '',
|
|
535
605
|
'音乐封面': p.music.cover || '',
|
|
536
|
-
'音乐链接': p.music.url || '',
|
|
537
606
|
};
|
|
538
607
|
const lines = format.split('\n');
|
|
539
608
|
const resultLines = [];
|
|
@@ -605,45 +674,50 @@ function apply(ctx, config) {
|
|
|
605
674
|
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
606
675
|
};
|
|
607
676
|
const proxyConfig = config.proxy || {};
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
protocol: proxyConfig.protocol || 'http',
|
|
619
|
-
host: proxyConfig.host,
|
|
620
|
-
port: proxyConfig.port || 7890,
|
|
621
|
-
auth: proxyConfig.auth?.username ? {
|
|
622
|
-
username: proxyConfig.auth.username,
|
|
623
|
-
password: proxyConfig.auth.password || ''
|
|
624
|
-
} : undefined
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
const http = axios_1.default.create(axiosConfig);
|
|
628
|
-
const defaultDedicatedApis = {
|
|
629
|
-
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
630
|
-
douyin: 'https://api.bugpk.com/api/douyin',
|
|
631
|
-
doubao: 'https://api.bugpk.com/api/dbvideos',
|
|
632
|
-
doubao_chat: 'https://api.bugpk.com/api/dbduihua',
|
|
633
|
-
kuaishou: 'https://api.bugpk.com/api/kuaishou',
|
|
634
|
-
xiaohongshu: 'https://api.bugpk.com/api/xhs',
|
|
635
|
-
jimeng: 'https://api.bugpk.com/api/jimengai',
|
|
636
|
-
toutiao: 'https://api.bugpk.com/api/toutiao',
|
|
637
|
-
weibo: 'https://api.bugpk.com/api/weibo',
|
|
638
|
-
huya: 'https://api.bugpk.com/api/huya',
|
|
639
|
-
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
640
|
-
pipixia: 'https://api.bugpk.com/api/pipixia',
|
|
641
|
-
zuiyou: 'https://api.bugpk.com/api/zuiyou',
|
|
642
|
-
wechat_channel: 'https://api.bugpk.com/api/wxsph',
|
|
643
|
-
};
|
|
644
|
-
const backupSupportedPlatforms = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']);
|
|
677
|
+
const customPlatforms = (config.customPlatforms || []).map((p) => ({
|
|
678
|
+
name: p.name,
|
|
679
|
+
apiUrl: p.apiUrl,
|
|
680
|
+
apiKey: p.apiKey || '',
|
|
681
|
+
authHeaderType: p.authHeaderType || 'Bearer',
|
|
682
|
+
customHeaderName: p.customHeaderName || 'X-API-Key',
|
|
683
|
+
fieldMapping: parseFieldMapping(p.fieldMapping),
|
|
684
|
+
proxy: p.proxy || null
|
|
685
|
+
}));
|
|
686
|
+
const downloadLimiter = new ConcurrencyLimiter(config.downloadConcurrency || 3);
|
|
645
687
|
function getPlatformConfig(type) {
|
|
688
|
+
if (type.startsWith('custom_')) {
|
|
689
|
+
const name = type.slice(7);
|
|
690
|
+
const custom = customPlatforms.find(p => p.name === name);
|
|
691
|
+
if (custom) {
|
|
692
|
+
return {
|
|
693
|
+
apiUrl: custom.apiUrl,
|
|
694
|
+
dedicatedFirst: true,
|
|
695
|
+
apiKey: custom.apiKey,
|
|
696
|
+
authHeaderType: custom.authHeaderType,
|
|
697
|
+
customHeaderName: custom.customHeaderName,
|
|
698
|
+
fieldMapping: custom.fieldMapping,
|
|
699
|
+
customProxy: custom.proxy
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return { apiUrl: null, dedicatedFirst: false, apiKey: '', authHeaderType: 'Bearer', customHeaderName: 'X-API-Key' };
|
|
703
|
+
}
|
|
646
704
|
const custom = config.customApis?.find((item) => item.platform === type);
|
|
705
|
+
const defaultDedicatedApis = {
|
|
706
|
+
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
707
|
+
douyin: 'https://api.bugpk.com/api/douyin',
|
|
708
|
+
doubao: 'https://api.bugpk.com/api/dbvideos',
|
|
709
|
+
doubao_chat: 'https://api.bugpk.com/api/dbduihua',
|
|
710
|
+
kuaishou: 'https://api.bugpk.com/api/kuaishou',
|
|
711
|
+
xiaohongshu: 'https://api.bugpk.com/api/xhs',
|
|
712
|
+
jimeng: 'https://api.bugpk.com/api/jimengai',
|
|
713
|
+
toutiao: 'https://api.bugpk.com/api/toutiao',
|
|
714
|
+
weibo: 'https://api.bugpk.com/api/weibo',
|
|
715
|
+
huya: 'https://api.bugpk.com/api/huya',
|
|
716
|
+
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
717
|
+
pipixia: 'https://api.bugpk.com/api/pipixia',
|
|
718
|
+
zuiyou: 'https://api.bugpk.com/api/zuiyou',
|
|
719
|
+
wechat_channel: 'https://api.bugpk.com/api/wxsph',
|
|
720
|
+
};
|
|
647
721
|
let apiUrl = defaultDedicatedApis[type] || null;
|
|
648
722
|
let apiKey = '';
|
|
649
723
|
let authHeaderType = 'Bearer';
|
|
@@ -688,63 +762,21 @@ function apply(ctx, config) {
|
|
|
688
762
|
return cleanUrl(url);
|
|
689
763
|
}
|
|
690
764
|
}
|
|
691
|
-
async function
|
|
692
|
-
if (!
|
|
693
|
-
throw new Error('
|
|
694
|
-
const tempDir = config.tempDir || './temp_videos';
|
|
765
|
+
async function downloadFile(url, timeout, maxSize, tempDir, filePrefix, fileExts) {
|
|
766
|
+
if (!url)
|
|
767
|
+
throw new Error('链接为空');
|
|
695
768
|
await promises_1.default.mkdir(tempDir, { recursive: true });
|
|
696
|
-
const
|
|
769
|
+
const ext = fileExts.find(e => url.match(new RegExp('\\.' + e + '(\\?|$)', 'i'))) || fileExts[0];
|
|
770
|
+
const fileName = `${filePrefix}_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
|
|
697
771
|
const filePath = path_1.default.resolve(tempDir, fileName);
|
|
698
772
|
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
699
773
|
let response;
|
|
700
774
|
try {
|
|
701
775
|
response = await http({
|
|
702
776
|
method: 'GET',
|
|
703
|
-
url
|
|
777
|
+
url,
|
|
704
778
|
responseType: 'stream',
|
|
705
|
-
timeout
|
|
706
|
-
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
|
|
707
|
-
maxRedirects: 5,
|
|
708
|
-
validateStatus: (status) => status >= 200 && status < 300,
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
catch (e) {
|
|
712
|
-
writer.destroy();
|
|
713
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
714
|
-
throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
|
|
715
|
-
}
|
|
716
|
-
const maxSizeBytes = (config.maxVideoSize ?? 0) * 1024 * 1024;
|
|
717
|
-
const contentLength = Number(response.headers['content-length'] || 0);
|
|
718
|
-
if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
|
|
719
|
-
writer.destroy();
|
|
720
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
721
|
-
throw new Error(`视频文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxVideoSize}MB)`);
|
|
722
|
-
}
|
|
723
|
-
try {
|
|
724
|
-
await (0, promises_2.pipeline)(response.data, writer);
|
|
725
|
-
return filePath;
|
|
726
|
-
}
|
|
727
|
-
catch (e) {
|
|
728
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
729
|
-
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
async function downloadImageFile(imageUrl) {
|
|
733
|
-
if (!imageUrl)
|
|
734
|
-
throw new Error('图片链接为空');
|
|
735
|
-
const imgTempDir = config.imageTempDir || './temp_images';
|
|
736
|
-
await promises_1.default.mkdir(imgTempDir, { recursive: true });
|
|
737
|
-
const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp)(\?|$)/i)?.[1] || 'jpg';
|
|
738
|
-
const fileName = `img_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
|
|
739
|
-
const filePath = path_1.default.resolve(imgTempDir, fileName);
|
|
740
|
-
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
741
|
-
let response;
|
|
742
|
-
try {
|
|
743
|
-
response = await http({
|
|
744
|
-
method: 'GET',
|
|
745
|
-
url: imageUrl,
|
|
746
|
-
responseType: 'stream',
|
|
747
|
-
timeout: config.imageDownloadTimeout || 60000,
|
|
779
|
+
timeout,
|
|
748
780
|
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
|
|
749
781
|
maxRedirects: 5,
|
|
750
782
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
@@ -753,14 +785,14 @@ function apply(ctx, config) {
|
|
|
753
785
|
catch (e) {
|
|
754
786
|
writer.destroy();
|
|
755
787
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
756
|
-
throw new Error(
|
|
788
|
+
throw new Error(`下载失败: ${getErrorMessage(e)}`);
|
|
757
789
|
}
|
|
758
|
-
const maxSizeBytes =
|
|
790
|
+
const maxSizeBytes = maxSize * 1024 * 1024;
|
|
759
791
|
const contentLength = Number(response.headers['content-length'] || 0);
|
|
760
792
|
if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
|
|
761
793
|
writer.destroy();
|
|
762
794
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
763
|
-
throw new Error(
|
|
795
|
+
throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
|
|
764
796
|
}
|
|
765
797
|
try {
|
|
766
798
|
await (0, promises_2.pipeline)(response.data, writer);
|
|
@@ -768,122 +800,68 @@ function apply(ctx, config) {
|
|
|
768
800
|
}
|
|
769
801
|
catch (e) {
|
|
770
802
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
771
|
-
throw new Error(
|
|
803
|
+
throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
|
|
772
804
|
}
|
|
773
805
|
}
|
|
774
|
-
async function
|
|
775
|
-
if (!
|
|
806
|
+
async function sendMedia(session, url, type, forceDownload, showFile, timeout, tempDir, maxSize) {
|
|
807
|
+
if (!url)
|
|
776
808
|
return;
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
809
|
+
await downloadLimiter.acquire();
|
|
810
|
+
try {
|
|
811
|
+
const sendLink = async () => { await sendWithTimeout(session, `${type === 'audio' ? '音乐' : type === 'video' ? '视频' : '图片'}链接:${url}`).catch(() => { }); };
|
|
812
|
+
const extMap = {
|
|
813
|
+
image: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
|
|
814
|
+
video: ['mp4'],
|
|
815
|
+
audio: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']
|
|
816
|
+
};
|
|
817
|
+
const prefixMap = { image: 'img', video: 'video', audio: 'music' };
|
|
818
|
+
const sendFunc = type === 'audio' ? koishi_1.h.audio : type === 'video' ? koishi_1.h.video : koishi_1.h.image;
|
|
819
|
+
if (forceDownload) {
|
|
786
820
|
try {
|
|
787
|
-
await
|
|
821
|
+
const localPath = await downloadFile(url, timeout, maxSize, tempDir, prefixMap[type], extMap[type]);
|
|
822
|
+
try {
|
|
823
|
+
await sendWithTimeout(session, sendFunc(`file://${localPath}`));
|
|
824
|
+
}
|
|
825
|
+
finally {
|
|
826
|
+
await promises_1.default.unlink(localPath).catch(() => { });
|
|
827
|
+
}
|
|
788
828
|
return;
|
|
789
829
|
}
|
|
790
|
-
catch {
|
|
791
|
-
|
|
830
|
+
catch (e) {
|
|
831
|
+
debugLog('ERROR', `强制下载${type}失败,尝试URL发送:`, getErrorMessage(e));
|
|
832
|
+
try {
|
|
833
|
+
await sendWithTimeout(session, sendFunc(url));
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
await sendLink();
|
|
837
|
+
}
|
|
792
838
|
}
|
|
839
|
+
return;
|
|
793
840
|
}
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
if (!config.showImageFile) {
|
|
797
|
-
await sendLink();
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
try {
|
|
801
|
-
await sendWithTimeout(session, koishi_1.h.image(imageUrl));
|
|
802
|
-
}
|
|
803
|
-
catch {
|
|
804
|
-
try {
|
|
805
|
-
const localPath = await downloadImageFile(imageUrl);
|
|
806
|
-
await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
|
|
807
|
-
}
|
|
808
|
-
catch {
|
|
841
|
+
if (!showFile) {
|
|
809
842
|
await sendLink();
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
async function sendMusicCover(session, imageUrl) {
|
|
814
|
-
if (!config.showMusicCover)
|
|
815
|
-
return;
|
|
816
|
-
const sendLink = async () => { await sendWithTimeout(session, `图片链接:${imageUrl}`).catch(() => { }); };
|
|
817
|
-
if (config.forceDownloadImage) {
|
|
818
|
-
try {
|
|
819
|
-
const localPath = await downloadImageFile(imageUrl);
|
|
820
|
-
await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
|
|
821
843
|
return;
|
|
822
844
|
}
|
|
823
|
-
catch (e) {
|
|
824
|
-
debugLog('ERROR', '强制下载音乐封面失败,尝试URL发送:', getErrorMessage(e));
|
|
825
|
-
try {
|
|
826
|
-
await sendWithTimeout(session, koishi_1.h.image(imageUrl));
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
catch {
|
|
830
|
-
await sendLink();
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
if (!config.showImageFile) {
|
|
836
|
-
await sendLink();
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
try {
|
|
840
|
-
await sendWithTimeout(session, koishi_1.h.image(imageUrl));
|
|
841
|
-
}
|
|
842
|
-
catch {
|
|
843
845
|
try {
|
|
844
|
-
|
|
845
|
-
await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
|
|
846
|
+
await sendWithTimeout(session, sendFunc(url));
|
|
846
847
|
}
|
|
847
848
|
catch {
|
|
848
|
-
await sendLink();
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
async function sendVideoFile(session, videoUrl) {
|
|
853
|
-
if (!videoUrl)
|
|
854
|
-
return;
|
|
855
|
-
if (!config.showVideoFile)
|
|
856
|
-
return await sendWithTimeout(session, `视频链接:${videoUrl}`);
|
|
857
|
-
const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
|
|
858
|
-
if (config.forceDownloadVideo) {
|
|
859
|
-
try {
|
|
860
|
-
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
861
|
-
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
catch (e) {
|
|
865
|
-
debugLog('ERROR', '强制下载视频失败,尝试URL发送:', getErrorMessage(e));
|
|
866
849
|
try {
|
|
867
|
-
await
|
|
868
|
-
|
|
850
|
+
const localPath = await downloadFile(url, timeout, maxSize, tempDir, prefixMap[type], extMap[type]);
|
|
851
|
+
try {
|
|
852
|
+
await sendWithTimeout(session, sendFunc(`file://${localPath}`));
|
|
853
|
+
}
|
|
854
|
+
finally {
|
|
855
|
+
await promises_1.default.unlink(localPath).catch(() => { });
|
|
856
|
+
}
|
|
869
857
|
}
|
|
870
858
|
catch {
|
|
871
859
|
await sendLink();
|
|
872
860
|
}
|
|
873
861
|
}
|
|
874
|
-
return;
|
|
875
862
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
catch {
|
|
880
|
-
try {
|
|
881
|
-
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
882
|
-
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
883
|
-
}
|
|
884
|
-
catch {
|
|
885
|
-
await sendLink();
|
|
886
|
-
}
|
|
863
|
+
finally {
|
|
864
|
+
downloadLimiter.release();
|
|
887
865
|
}
|
|
888
866
|
}
|
|
889
867
|
async function flush(session, matches) {
|
|
@@ -894,6 +872,11 @@ function apply(ctx, config) {
|
|
|
894
872
|
const promises = matches.map(async (match) => {
|
|
895
873
|
await limiter.acquire();
|
|
896
874
|
try {
|
|
875
|
+
const platformEnabled = config.platformEnabled?.[match.type] ?? true;
|
|
876
|
+
if (!platformEnabled && !match.type.startsWith('custom_')) {
|
|
877
|
+
debugLog('INFO', `平台 ${match.type} 已禁用,跳过链接: ${match.url}`);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
897
880
|
if (config.deduplicationInterval > 0) {
|
|
898
881
|
const lastTime = dedupCache.get(match.url);
|
|
899
882
|
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
@@ -904,8 +887,9 @@ function apply(ctx, config) {
|
|
|
904
887
|
}
|
|
905
888
|
}
|
|
906
889
|
debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
|
|
907
|
-
const
|
|
908
|
-
const
|
|
890
|
+
const platformConf = getPlatformConfig(match.type);
|
|
891
|
+
const fieldMapping = platformConf.fieldMapping;
|
|
892
|
+
const result = await processSingleUrl(match.url, match.type, fieldMapping, platformConf);
|
|
909
893
|
if (result.success) {
|
|
910
894
|
items.push(result.data);
|
|
911
895
|
if (config.deduplicationInterval > 0)
|
|
@@ -925,7 +909,7 @@ function apply(ctx, config) {
|
|
|
925
909
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
926
910
|
if (!items.length)
|
|
927
911
|
return;
|
|
928
|
-
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
912
|
+
const enableForward = config.enableForward && (session.platform === 'onebot' || session.platform === 'satori');
|
|
929
913
|
const botName = config.botName || '视频解析机器人';
|
|
930
914
|
if (enableForward) {
|
|
931
915
|
const forwardMessages = [];
|
|
@@ -947,6 +931,9 @@ function apply(ctx, config) {
|
|
|
947
931
|
}
|
|
948
932
|
if (p.video)
|
|
949
933
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
|
|
934
|
+
if (config.showMusicVoice && p.music.url) {
|
|
935
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.audio(p.music.url), botName));
|
|
936
|
+
}
|
|
950
937
|
}
|
|
951
938
|
if (forwardMessages.length) {
|
|
952
939
|
try {
|
|
@@ -970,37 +957,41 @@ function apply(ctx, config) {
|
|
|
970
957
|
await delay(300);
|
|
971
958
|
}
|
|
972
959
|
if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
973
|
-
await
|
|
960
|
+
await sendMedia(session, p.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
|
|
974
961
|
await delay(300);
|
|
975
962
|
}
|
|
976
963
|
if (config.showMusicCover && p.music.cover) {
|
|
977
|
-
await
|
|
964
|
+
await sendMedia(session, p.music.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
|
|
978
965
|
await delay(300);
|
|
979
966
|
}
|
|
980
967
|
if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
981
|
-
await
|
|
968
|
+
await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile, config.videoDownloadTimeout, config.tempDir, config.maxVideoSize).catch(() => { });
|
|
982
969
|
await delay(500);
|
|
983
970
|
}
|
|
984
971
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
985
972
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
986
973
|
for (const imgUrl of imageUrls) {
|
|
987
|
-
await
|
|
974
|
+
await sendMedia(session, imgUrl, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
|
|
988
975
|
await delay(200);
|
|
989
976
|
}
|
|
990
977
|
}
|
|
978
|
+
if (config.showMusicVoice && p.music.url) {
|
|
979
|
+
await sendMedia(session, p.music.url, 'audio', config.forceDownloadMusicVoice, config.showMusicVoiceFile, config.musicDownloadTimeout, config.musicTempDir, config.maxMusicSize).catch(() => { });
|
|
980
|
+
await delay(300);
|
|
981
|
+
}
|
|
991
982
|
}
|
|
992
983
|
}
|
|
993
984
|
debugLog('INFO', '处理完成');
|
|
994
985
|
}
|
|
995
|
-
async function fetchApi(url, type, fieldMapping) {
|
|
986
|
+
async function fetchApi(url, type, fieldMapping, platformConf) {
|
|
996
987
|
const cacheKey = url;
|
|
997
988
|
const cached = urlCacheLocal.get(cacheKey);
|
|
998
989
|
if (cached && cached.expire > Date.now())
|
|
999
990
|
return cached.data;
|
|
1000
|
-
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
|
|
991
|
+
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, customProxy } = platformConf || getPlatformConfig(type);
|
|
1001
992
|
const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
|
|
1002
993
|
const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
|
|
1003
|
-
const backupAllowed =
|
|
994
|
+
const backupAllowed = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']).has(type);
|
|
1004
995
|
const apiList = [];
|
|
1005
996
|
if (dedicatedFirst && dedicatedUrl) {
|
|
1006
997
|
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
@@ -1015,6 +1006,9 @@ function apply(ctx, config) {
|
|
|
1015
1006
|
if (dedicatedUrl)
|
|
1016
1007
|
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
1017
1008
|
}
|
|
1009
|
+
if (type.startsWith('custom_') && apiList.length === 0 && dedicatedUrl) {
|
|
1010
|
+
apiList.push({ url: dedicatedUrl, label: `自定义API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
1011
|
+
}
|
|
1018
1012
|
const customHeaders = config.customHeaders || [];
|
|
1019
1013
|
let lastError = null;
|
|
1020
1014
|
for (const api of apiList) {
|
|
@@ -1033,7 +1027,19 @@ function apply(ctx, config) {
|
|
|
1033
1027
|
const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
|
|
1034
1028
|
Object.assign(headers, authHeaders);
|
|
1035
1029
|
}
|
|
1036
|
-
const
|
|
1030
|
+
const proxyToUse = customProxy && customProxy.enabled ? customProxy : (proxyConfig.enabled ? proxyConfig : undefined);
|
|
1031
|
+
const axiosConfigLocal = {
|
|
1032
|
+
params: { url },
|
|
1033
|
+
timeout: config.timeout,
|
|
1034
|
+
headers,
|
|
1035
|
+
proxy: proxyToUse && proxyToUse.host ? {
|
|
1036
|
+
protocol: proxyToUse.protocol || 'http',
|
|
1037
|
+
host: proxyToUse.host,
|
|
1038
|
+
port: proxyToUse.port || 7890,
|
|
1039
|
+
auth: proxyToUse.auth?.username ? { username: proxyToUse.auth.username, password: proxyToUse.auth.password || '' } : undefined
|
|
1040
|
+
} : undefined
|
|
1041
|
+
};
|
|
1042
|
+
const res = await http.get(api.url, axiosConfigLocal);
|
|
1037
1043
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
1038
1044
|
const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
|
|
1039
1045
|
urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
|
|
@@ -1052,12 +1058,12 @@ function apply(ctx, config) {
|
|
|
1052
1058
|
}
|
|
1053
1059
|
throw lastError || new Error('所有API请求全部失败');
|
|
1054
1060
|
}
|
|
1055
|
-
async function parseUrl(url, type, fieldMapping) {
|
|
1061
|
+
async function parseUrl(url, type, fieldMapping, platformConf) {
|
|
1056
1062
|
const realUrl = await resolveShortUrl(url);
|
|
1057
1063
|
const candidates = [...new Set([realUrl, url])];
|
|
1058
1064
|
for (const candidate of candidates) {
|
|
1059
1065
|
try {
|
|
1060
|
-
const info = await fetchApi(candidate, type, fieldMapping);
|
|
1066
|
+
const info = await fetchApi(candidate, type, fieldMapping, platformConf);
|
|
1061
1067
|
if (info.video || info.images.length > 0 || info.live_photo.length > 0)
|
|
1062
1068
|
return { success: true, data: info };
|
|
1063
1069
|
debugLog('WARN', `解析成功但无内容: ${candidate}`);
|
|
@@ -1068,8 +1074,8 @@ function apply(ctx, config) {
|
|
|
1068
1074
|
}
|
|
1069
1075
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
1070
1076
|
}
|
|
1071
|
-
async function processSingleUrl(url, type, fieldMapping) {
|
|
1072
|
-
const result = await parseUrl(url, type, fieldMapping);
|
|
1077
|
+
async function processSingleUrl(url, type, fieldMapping, platformConf) {
|
|
1078
|
+
const result = await parseUrl(url, type, fieldMapping, platformConf);
|
|
1073
1079
|
if (!result.success)
|
|
1074
1080
|
return { success: false, msg: result.msg, url };
|
|
1075
1081
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
@@ -1100,6 +1106,27 @@ function apply(ctx, config) {
|
|
|
1100
1106
|
}
|
|
1101
1107
|
return null;
|
|
1102
1108
|
}
|
|
1109
|
+
const customRules = buildCustomLinkRules(config.customPlatforms || []);
|
|
1110
|
+
const axiosConfig = {
|
|
1111
|
+
timeout: config.timeout,
|
|
1112
|
+
headers: {
|
|
1113
|
+
'User-Agent': config.userAgent,
|
|
1114
|
+
'Referer': 'https://www.baidu.com/',
|
|
1115
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
if (proxyConfig.enabled && proxyConfig.host) {
|
|
1119
|
+
axiosConfig.proxy = {
|
|
1120
|
+
protocol: proxyConfig.protocol || 'http',
|
|
1121
|
+
host: proxyConfig.host,
|
|
1122
|
+
port: proxyConfig.port || 7890,
|
|
1123
|
+
auth: proxyConfig.auth?.username ? {
|
|
1124
|
+
username: proxyConfig.auth.username,
|
|
1125
|
+
password: proxyConfig.auth.password || ''
|
|
1126
|
+
} : undefined
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const http = axios_1.default.create(axiosConfig);
|
|
1103
1130
|
ctx.on('message', async (session) => {
|
|
1104
1131
|
if (!config.enable)
|
|
1105
1132
|
return;
|
|
@@ -1109,7 +1136,7 @@ function apply(ctx, config) {
|
|
|
1109
1136
|
return;
|
|
1110
1137
|
if (session.selfId === session.userId)
|
|
1111
1138
|
return;
|
|
1112
|
-
const matches = extractAllUrlsFromMessage(session);
|
|
1139
|
+
const matches = extractAllUrlsFromMessage(session, customRules);
|
|
1113
1140
|
if (!matches.length)
|
|
1114
1141
|
return;
|
|
1115
1142
|
debugLog('INFO', `检测到 ${matches.length} 个链接`);
|
|
@@ -1128,7 +1155,7 @@ function apply(ctx, config) {
|
|
|
1128
1155
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
1129
1156
|
return;
|
|
1130
1157
|
}
|
|
1131
|
-
const matches = linkTypeParser(url);
|
|
1158
|
+
const matches = linkTypeParser(url, customRules);
|
|
1132
1159
|
if (!matches.length) {
|
|
1133
1160
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
1134
1161
|
return;
|
|
@@ -1143,12 +1170,14 @@ function apply(ctx, config) {
|
|
|
1143
1170
|
});
|
|
1144
1171
|
const tempCleanupInterval = setInterval(async () => {
|
|
1145
1172
|
try {
|
|
1146
|
-
const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
|
|
1173
|
+
const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images', config.musicTempDir || './temp_music'];
|
|
1147
1174
|
for (const dir of dirs) {
|
|
1148
1175
|
const files = await promises_1.default.readdir(dir);
|
|
1149
1176
|
const now = Date.now();
|
|
1150
1177
|
for (const file of files) {
|
|
1151
|
-
if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
|
|
1178
|
+
if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
|
|
1179
|
+
(file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
|
|
1180
|
+
(file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
|
|
1152
1181
|
const filePath = path_1.default.join(dir, file);
|
|
1153
1182
|
const stats = await promises_1.default.stat(filePath);
|
|
1154
1183
|
if (now - stats.mtimeMs > 3600000) {
|
|
@@ -1170,11 +1199,13 @@ function apply(ctx, config) {
|
|
|
1170
1199
|
});
|
|
1171
1200
|
process.on('beforeExit', async () => {
|
|
1172
1201
|
try {
|
|
1173
|
-
const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
|
|
1202
|
+
const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images', config.musicTempDir || './temp_music'];
|
|
1174
1203
|
for (const dir of dirs) {
|
|
1175
1204
|
const files = await promises_1.default.readdir(dir);
|
|
1176
1205
|
for (const file of files) {
|
|
1177
|
-
if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
|
|
1206
|
+
if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
|
|
1207
|
+
(file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
|
|
1208
|
+
(file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
|
|
1178
1209
|
await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
|
|
1179
1210
|
}
|
|
1180
1211
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-video-parser-all",
|
|
3
3
|
"description": "Koishi 全平台视频/图集解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.5",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
"多平台",
|
|
59
59
|
"图集解析",
|
|
60
60
|
"去水印",
|
|
61
|
+
"代理",
|
|
62
|
+
"字段映射",
|
|
61
63
|
"哔哩哔哩",
|
|
62
64
|
"抖音",
|
|
63
65
|
"快手",
|
|
@@ -97,7 +99,17 @@
|
|
|
97
99
|
},
|
|
98
100
|
"peerDependencies": {
|
|
99
101
|
"@koishijs/plugin-console": "^5.30.4",
|
|
100
|
-
"koishi": "^4.18.7"
|
|
102
|
+
"koishi": "^4.18.7",
|
|
103
|
+
"koishi-plugin-silk": "^1.0.0",
|
|
104
|
+
"koishi-plugin-ffmpeg": "^1.0.0"
|
|
105
|
+
},
|
|
106
|
+
"peerDependenciesMeta": {
|
|
107
|
+
"koishi-plugin-silk": {
|
|
108
|
+
"optional": true
|
|
109
|
+
},
|
|
110
|
+
"koishi-plugin-ffmpeg": {
|
|
111
|
+
"optional": true
|
|
112
|
+
}
|
|
101
113
|
},
|
|
102
114
|
"repository": {
|
|
103
115
|
"type": "git",
|
package/readme.md
CHANGED
|
@@ -27,11 +27,12 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
27
27
|
| `botName` | string | 视频解析机器人 | 合并转发消息中显示的机器人名称 |
|
|
28
28
|
| `showWaitingTip` | boolean | true | 解析时是否显示等待提示 |
|
|
29
29
|
| `debug` | boolean | false | 是否开启 Debug 模式,在控制台输出详细日志 |
|
|
30
|
+
| `platformEnabled` | object | 各平台均为 `true` | 各平台解析开关,可单独关闭某平台 |
|
|
30
31
|
|
|
31
32
|
### 统一消息格式 (Unified Message Format)
|
|
32
33
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
33
34
|
|--------|------|--------|------|
|
|
34
|
-
| `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n
|
|
35
|
+
| `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 文字消息格式,支持变量替换。空行自动隐藏。 |
|
|
35
36
|
|
|
36
37
|
### 内容显示设置 (Content Display Settings)
|
|
37
38
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -50,7 +51,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
50
51
|
| `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
|
|
51
52
|
| `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 不限制 |
|
|
52
53
|
| `maxDescLength` | number | 200 | 简介最大长度(字符) |
|
|
53
|
-
| `maxConcurrent` | number | 3 |
|
|
54
|
+
| `maxConcurrent` | number | 3 | 解析最大并发数 |
|
|
55
|
+
| `downloadConcurrency` | number | 3 | 下载线程数(≥1 整数) |
|
|
56
|
+
| `showMusicVoice` | boolean | false | 是否发送音乐(转语音)。**需要依赖 `koishi-plugin-silk` 和 `koishi-plugin-ffmpeg`** |
|
|
57
|
+
| `showMusicVoiceFile` | boolean | true | 音乐语音是否以文件形式发送(关闭则只发链接) |
|
|
58
|
+
| `forceDownloadMusicVoice` | boolean | false | 强制下载音乐语音后发送 |
|
|
59
|
+
| `musicDownloadTimeout` | number | 120000 | 音乐下载超时(毫秒) |
|
|
60
|
+
| `musicTempDir` | string | `./temp_music` | 临时音乐存储目录 |
|
|
61
|
+
| `maxMusicSize` | number | 0 | 最大下载音乐大小(MB),0 不限制 |
|
|
54
62
|
|
|
55
63
|
### 网络与 API 设置 (Network & API Settings)
|
|
56
64
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -64,10 +72,9 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
64
72
|
### API 选择与回退设置 (API Selection & Fallback)
|
|
65
73
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
66
74
|
|--------|------|--------|------|
|
|
67
|
-
| `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址 |
|
|
68
|
-
| `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API,仅支持部分平台 |
|
|
69
75
|
| `platformDedicatedFirst` | object | 各平台均为 `false` | 平台专属 API 优先开关,键:`bilibili` 等 |
|
|
70
|
-
| `customApis` | array | [] |
|
|
76
|
+
| `customApis` | array | [] | 自定义内置平台专属 API,含 `platform`, `apiUrl`, `apiKey`, `authHeaderType`, `customHeaderName`, `fieldMapping` |
|
|
77
|
+
| `customPlatforms` | array | [] | 完全自定义新平台。每项含:`name`(平台名称)、`exampleUrl`(示例链接)、`keywords`(匹配关键词,逗号分隔)、`apiUrl`(解析API)、`apiKey`、`authHeaderType`、`customHeaderName`、`fieldMapping`、`proxy`(独立代理) |
|
|
71
78
|
| `globalFieldMapping` | string | 预设字段映射 JSON | 全局字段映射,支持点号路径 |
|
|
72
79
|
|
|
73
80
|
### 错误与重试设置 (Error & Retry Settings)
|
|
@@ -80,7 +87,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
80
87
|
### 发送方式设置 (Send Mode Settings)
|
|
81
88
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
82
89
|
|--------|------|--------|------|
|
|
83
|
-
| `enableForward` | boolean | false |
|
|
90
|
+
| `enableForward` | boolean | false | 启用合并转发(支持 OneBot、Satori 平台) |
|
|
84
91
|
|
|
85
92
|
### 缓存与去重设置 (Cache & Deduplication Settings)
|
|
86
93
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -117,9 +124,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
117
124
|
| `${视频链接}` | 视频原始链接 |
|
|
118
125
|
| `${音乐标题}` | 音乐标题 |
|
|
119
126
|
| `${音乐作者}` | 音乐作者 |
|
|
120
|
-
| `${音乐链接}` | 音乐原始链接 |
|
|
121
127
|
|
|
122
|
-
|
|
128
|
+
## 音乐语音依赖说明 (Music Voice Dependencies)
|
|
129
|
+
若启用 `showMusicVoice`,请确保已安装以下 Koishi 插件:
|
|
130
|
+
- `koishi-plugin-silk`:提供 silk 编解码支持
|
|
131
|
+
- `koishi-plugin-ffmpeg`:提供音频重采样支持
|
|
132
|
+
|
|
133
|
+
这些依赖已声明为可选依赖
|
|
134
|
+
若未安装,音乐语音将尝试直接发送原始音频,部分平台可能无法播放。
|
|
123
135
|
|
|
124
136
|
## 支持的平台 (Supported Platforms)
|
|
125
137
|
| 平台名称 | 关键词识别 | 解析能力 |
|
|
@@ -131,12 +143,12 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
131
143
|
| 微博 | weibo, video.weibo.com | 视频、图集 |
|
|
132
144
|
| 剪映 / 即梦 | jianying, jimeng.jianying.com | 视频模板 |
|
|
133
145
|
| 今日头条 / 西瓜视频 | toutiao, ixigua.com | 短视频 |
|
|
134
|
-
| AcFun
|
|
146
|
+
| AcFun(A站) | acfun, acfun.cn | 视频 |
|
|
135
147
|
| 知乎 | zhihu, zhihu.com | 视频、回答 |
|
|
136
148
|
| 微视 | weishi, weishi.qq.com | 短视频 |
|
|
137
149
|
| 虎牙 | huya, huya.com | 直播、视频 |
|
|
138
|
-
| YouTube
|
|
139
|
-
| TikTok
|
|
150
|
+
| YouTube(油管) | youtube, youtu.be | 视频 |
|
|
151
|
+
| TikTok(国际版抖音) | tiktok, tiktok.com | 短视频 |
|
|
140
152
|
| 好看视频 | haokan, haokan.baidu.com | 短视频 |
|
|
141
153
|
| 梨视频 | video.li | 短视频 |
|
|
142
154
|
| 美拍 | meipai, meipai.com | 短视频 |
|
|
@@ -150,8 +162,9 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
150
162
|
| 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
|
|
151
163
|
| 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
|
|
152
164
|
| 视频号 (WeChat Channels) | channels.weixin.qq.com, weixin.qq.com/sph/ | 短视频 |
|
|
165
|
+
| 🔧 自定义平台 | 通过 `customPlatforms` 添加 | 取决于 API |
|
|
153
166
|
|
|
154
|
-
> 注:部分平台解析能力可能因API
|
|
167
|
+
> 注:部分平台解析能力可能因API限制有所差异。可通过 `platformEnabled` 单独关闭。
|
|
155
168
|
|
|
156
169
|
## 项目贡献者 (Contributors)
|
|
157
170
|
|