koishi-plugin-video-parser-all 1.3.3 → 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 +115 -0
- package/lib/index.js +265 -187
- package/package.json +14 -2
- package/readme.md +32 -19
package/lib/index.d.ts
CHANGED
|
@@ -5,11 +5,34 @@ 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
|
} & {
|
|
11
33
|
showImageText?: boolean | null | undefined;
|
|
12
34
|
showCoverImage?: boolean | null | undefined;
|
|
35
|
+
showMusicCover?: boolean | null | undefined;
|
|
13
36
|
showImageFile?: boolean | null | undefined;
|
|
14
37
|
forceDownloadImage?: boolean | null | undefined;
|
|
15
38
|
imageDownloadTimeout?: number | null | undefined;
|
|
@@ -22,6 +45,13 @@ export declare const Config: Schema<{
|
|
|
22
45
|
maxVideoSize?: number | null | undefined;
|
|
23
46
|
maxDescLength?: number | null | undefined;
|
|
24
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;
|
|
25
55
|
} & {
|
|
26
56
|
timeout?: number | null | undefined;
|
|
27
57
|
videoSendTimeout?: number | null | undefined;
|
|
@@ -82,6 +112,26 @@ export declare const Config: Schema<{
|
|
|
82
112
|
customHeaderName?: string | null | undefined;
|
|
83
113
|
fieldMapping?: string | null | undefined;
|
|
84
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;
|
|
85
135
|
globalFieldMapping?: string | null | undefined;
|
|
86
136
|
} & {
|
|
87
137
|
waitingTipText?: string | null | undefined;
|
|
@@ -94,11 +144,34 @@ export declare const Config: Schema<{
|
|
|
94
144
|
botName: string;
|
|
95
145
|
showWaitingTip: boolean;
|
|
96
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
|
+
}>;
|
|
97
169
|
} & import("cosmokit").Dict & {
|
|
98
170
|
unifiedMessageFormat: string;
|
|
99
171
|
} & {
|
|
100
172
|
showImageText: boolean;
|
|
101
173
|
showCoverImage: boolean;
|
|
174
|
+
showMusicCover: boolean;
|
|
102
175
|
showImageFile: boolean;
|
|
103
176
|
forceDownloadImage: boolean;
|
|
104
177
|
imageDownloadTimeout: number;
|
|
@@ -111,6 +184,13 @@ export declare const Config: Schema<{
|
|
|
111
184
|
maxVideoSize: number;
|
|
112
185
|
maxDescLength: number;
|
|
113
186
|
maxConcurrent: number;
|
|
187
|
+
downloadConcurrency: number;
|
|
188
|
+
showMusicVoice: boolean;
|
|
189
|
+
showMusicVoiceFile: boolean;
|
|
190
|
+
forceDownloadMusicVoice: boolean;
|
|
191
|
+
musicDownloadTimeout: number;
|
|
192
|
+
musicTempDir: string;
|
|
193
|
+
maxMusicSize: number;
|
|
114
194
|
} & {
|
|
115
195
|
timeout: number;
|
|
116
196
|
videoSendTimeout: number;
|
|
@@ -174,6 +254,41 @@ export declare const Config: Schema<{
|
|
|
174
254
|
customHeaderName: Schema<string, string>;
|
|
175
255
|
fieldMapping: Schema<string, string>;
|
|
176
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
|
+
}>[];
|
|
177
292
|
globalFieldMapping: string;
|
|
178
293
|
} & {
|
|
179
294
|
waitingTipText: string;
|
package/lib/index.js
CHANGED
|
@@ -74,13 +74,36 @@ 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('是否发送文字内容'),
|
|
83
|
-
showCoverImage: koishi_1.Schema.boolean().default(true).description('
|
|
105
|
+
showCoverImage: koishi_1.Schema.boolean().default(true).description('是否发送封面图片(视频/图集封面)'),
|
|
106
|
+
showMusicCover: koishi_1.Schema.boolean().default(true).description('是否发送音乐封面图片'),
|
|
84
107
|
showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以文件形式发送(关闭则只发送链接)'),
|
|
85
108
|
forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片后发送'),
|
|
86
109
|
imageDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('图片下载超时(毫秒)'),
|
|
@@ -92,7 +115,14 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
92
115
|
tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
|
|
93
116
|
maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
|
|
94
117
|
maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介最大长度(字符)'),
|
|
95
|
-
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 为不限制'),
|
|
96
126
|
}).description('内容显示设置'),
|
|
97
127
|
koishi_1.Schema.object({
|
|
98
128
|
timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
|
|
@@ -122,15 +152,15 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
122
152
|
retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
|
|
123
153
|
}).description('错误与重试'),
|
|
124
154
|
koishi_1.Schema.object({
|
|
125
|
-
enableForward: koishi_1.Schema.boolean().default(false).description('
|
|
155
|
+
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(支持 OneBot、Satori 平台)'),
|
|
126
156
|
}).description('发送方式'),
|
|
127
157
|
koishi_1.Schema.object({
|
|
128
158
|
deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
|
|
129
159
|
cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
|
|
130
160
|
}).description('缓存与去重'),
|
|
131
161
|
koishi_1.Schema.object({
|
|
132
|
-
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').
|
|
133
|
-
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(),
|
|
134
164
|
platformDedicatedFirst: koishi_1.Schema.object({
|
|
135
165
|
bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
|
|
136
166
|
douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
|
|
@@ -140,7 +170,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
140
170
|
xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
|
|
141
171
|
youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
|
|
142
172
|
tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
|
|
143
|
-
acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
|
|
173
|
+
acfun: koishi_1.Schema.boolean().default(false).description('AcFun(A站)'),
|
|
144
174
|
zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
|
|
145
175
|
weishi: koishi_1.Schema.boolean().default(false).description('微视'),
|
|
146
176
|
huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
|
|
@@ -163,7 +193,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
163
193
|
koishi_1.Schema.const('xigua').description('西瓜视频'),
|
|
164
194
|
koishi_1.Schema.const('youtube').description('YouTube'),
|
|
165
195
|
koishi_1.Schema.const('tiktok').description('TikTok'),
|
|
166
|
-
koishi_1.Schema.const('acfun').description('AcFun'),
|
|
196
|
+
koishi_1.Schema.const('acfun').description('AcFun(A站)'),
|
|
167
197
|
koishi_1.Schema.const('zhihu').description('知乎'),
|
|
168
198
|
koishi_1.Schema.const('weishi').description('微视'),
|
|
169
199
|
koishi_1.Schema.const('huya').description('虎牙'),
|
|
@@ -185,7 +215,34 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
185
215
|
]).default('Bearer').description('认证头类型'),
|
|
186
216
|
customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
|
|
187
217
|
fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
|
|
188
|
-
})).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('自定义新平台'),
|
|
189
246
|
globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
|
|
190
247
|
' "title": "data.title",\n' +
|
|
191
248
|
' "desc": "data.description",\n' +
|
|
@@ -225,7 +282,7 @@ function debugLog(level, ...args) {
|
|
|
225
282
|
return;
|
|
226
283
|
logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
|
|
227
284
|
}
|
|
228
|
-
const
|
|
285
|
+
const BUILTIN_LINK_RULES = [
|
|
229
286
|
{ pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
|
|
230
287
|
{ pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
|
|
231
288
|
{ pattern: /https?:\/\/bili\d+\.cn\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
|
|
@@ -257,11 +314,27 @@ const LINK_RULES = [
|
|
|
257
314
|
{ pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
258
315
|
{ pattern: /https?:\/\/weixin\.qq\.com\/sph\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
|
|
259
316
|
];
|
|
260
|
-
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) {
|
|
261
333
|
content = content.replace(/\\\//g, '/');
|
|
334
|
+
const allRules = [...BUILTIN_LINK_RULES, ...customRules];
|
|
262
335
|
const matches = [];
|
|
263
336
|
const seen = new Set();
|
|
264
|
-
for (const rule of
|
|
337
|
+
for (const rule of allRules) {
|
|
265
338
|
let match;
|
|
266
339
|
rule.pattern.lastIndex = 0;
|
|
267
340
|
while ((match = rule.pattern.exec(content)) !== null) {
|
|
@@ -274,9 +347,9 @@ function linkTypeParser(content) {
|
|
|
274
347
|
}
|
|
275
348
|
return matches;
|
|
276
349
|
}
|
|
277
|
-
function extractAllUrlsFromMessage(session) {
|
|
350
|
+
function extractAllUrlsFromMessage(session, customRules) {
|
|
278
351
|
const content = session.content?.trim() || '';
|
|
279
|
-
const matchedLinks = linkTypeParser(content);
|
|
352
|
+
const matchedLinks = linkTypeParser(content, customRules);
|
|
280
353
|
const cardsContent = [];
|
|
281
354
|
if (session.elements) {
|
|
282
355
|
for (const elem of session.elements) {
|
|
@@ -302,7 +375,7 @@ function extractAllUrlsFromMessage(session) {
|
|
|
302
375
|
}
|
|
303
376
|
}
|
|
304
377
|
for (const cardContent of cardsContent) {
|
|
305
|
-
matchedLinks.push(...linkTypeParser(cardContent));
|
|
378
|
+
matchedLinks.push(...linkTypeParser(cardContent, customRules));
|
|
306
379
|
}
|
|
307
380
|
const seen = new Set();
|
|
308
381
|
const result = [];
|
|
@@ -496,8 +569,6 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
|
|
|
496
569
|
const durRaw = mapField('duration', () => data.duration);
|
|
497
570
|
if (durRaw) {
|
|
498
571
|
duration = typeof durRaw === 'string' ? parseInt(durRaw, 10) : Number(durRaw);
|
|
499
|
-
if (duration > 3600)
|
|
500
|
-
duration = Math.floor(duration / 1000);
|
|
501
572
|
}
|
|
502
573
|
}
|
|
503
574
|
let publishTime = 0;
|
|
@@ -532,7 +603,6 @@ function generateFormattedText(p, format) {
|
|
|
532
603
|
'音乐标题': p.music.title || '',
|
|
533
604
|
'音乐作者': p.music.author || '',
|
|
534
605
|
'音乐封面': p.music.cover || '',
|
|
535
|
-
'音乐链接': p.music.url || '',
|
|
536
606
|
};
|
|
537
607
|
const lines = format.split('\n');
|
|
538
608
|
const resultLines = [];
|
|
@@ -604,45 +674,50 @@ function apply(ctx, config) {
|
|
|
604
674
|
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
605
675
|
};
|
|
606
676
|
const proxyConfig = config.proxy || {};
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
protocol: proxyConfig.protocol || 'http',
|
|
618
|
-
host: proxyConfig.host,
|
|
619
|
-
port: proxyConfig.port || 7890,
|
|
620
|
-
auth: proxyConfig.auth?.username ? {
|
|
621
|
-
username: proxyConfig.auth.username,
|
|
622
|
-
password: proxyConfig.auth.password || ''
|
|
623
|
-
} : undefined
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
const http = axios_1.default.create(axiosConfig);
|
|
627
|
-
const defaultDedicatedApis = {
|
|
628
|
-
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
629
|
-
douyin: 'https://api.bugpk.com/api/douyin',
|
|
630
|
-
doubao: 'https://api.bugpk.com/api/dbvideos',
|
|
631
|
-
doubao_chat: 'https://api.bugpk.com/api/dbduihua',
|
|
632
|
-
kuaishou: 'https://api.bugpk.com/api/kuaishou',
|
|
633
|
-
xiaohongshu: 'https://api.bugpk.com/api/xhs',
|
|
634
|
-
jimeng: 'https://api.bugpk.com/api/jimengai',
|
|
635
|
-
toutiao: 'https://api.bugpk.com/api/toutiao',
|
|
636
|
-
weibo: 'https://api.bugpk.com/api/weibo',
|
|
637
|
-
huya: 'https://api.bugpk.com/api/huya',
|
|
638
|
-
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
639
|
-
pipixia: 'https://api.bugpk.com/api/pipixia',
|
|
640
|
-
zuiyou: 'https://api.bugpk.com/api/zuiyou',
|
|
641
|
-
wechat_channel: 'https://api.bugpk.com/api/wxsph',
|
|
642
|
-
};
|
|
643
|
-
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);
|
|
644
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
|
+
}
|
|
645
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
|
+
};
|
|
646
721
|
let apiUrl = defaultDedicatedApis[type] || null;
|
|
647
722
|
let apiKey = '';
|
|
648
723
|
let authHeaderType = 'Bearer';
|
|
@@ -687,63 +762,21 @@ function apply(ctx, config) {
|
|
|
687
762
|
return cleanUrl(url);
|
|
688
763
|
}
|
|
689
764
|
}
|
|
690
|
-
async function
|
|
691
|
-
if (!
|
|
692
|
-
throw new Error('
|
|
693
|
-
const tempDir = config.tempDir || './temp_videos';
|
|
765
|
+
async function downloadFile(url, timeout, maxSize, tempDir, filePrefix, fileExts) {
|
|
766
|
+
if (!url)
|
|
767
|
+
throw new Error('链接为空');
|
|
694
768
|
await promises_1.default.mkdir(tempDir, { recursive: true });
|
|
695
|
-
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}`;
|
|
696
771
|
const filePath = path_1.default.resolve(tempDir, fileName);
|
|
697
772
|
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
698
773
|
let response;
|
|
699
774
|
try {
|
|
700
775
|
response = await http({
|
|
701
776
|
method: 'GET',
|
|
702
|
-
url
|
|
777
|
+
url,
|
|
703
778
|
responseType: 'stream',
|
|
704
|
-
timeout
|
|
705
|
-
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' },
|
|
706
|
-
maxRedirects: 5,
|
|
707
|
-
validateStatus: (status) => status >= 200 && status < 300,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
catch (e) {
|
|
711
|
-
writer.destroy();
|
|
712
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
713
|
-
throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
|
|
714
|
-
}
|
|
715
|
-
const maxSizeBytes = (config.maxVideoSize ?? 0) * 1024 * 1024;
|
|
716
|
-
const contentLength = Number(response.headers['content-length'] || 0);
|
|
717
|
-
if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
|
|
718
|
-
writer.destroy();
|
|
719
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
720
|
-
throw new Error(`视频文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxVideoSize}MB)`);
|
|
721
|
-
}
|
|
722
|
-
try {
|
|
723
|
-
await (0, promises_2.pipeline)(response.data, writer);
|
|
724
|
-
return filePath;
|
|
725
|
-
}
|
|
726
|
-
catch (e) {
|
|
727
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
728
|
-
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
async function downloadImageFile(imageUrl) {
|
|
732
|
-
if (!imageUrl)
|
|
733
|
-
throw new Error('图片链接为空');
|
|
734
|
-
const imgTempDir = config.imageTempDir || './temp_images';
|
|
735
|
-
await promises_1.default.mkdir(imgTempDir, { recursive: true });
|
|
736
|
-
const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp)(\?|$)/i)?.[1] || 'jpg';
|
|
737
|
-
const fileName = `img_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
|
|
738
|
-
const filePath = path_1.default.resolve(imgTempDir, fileName);
|
|
739
|
-
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
740
|
-
let response;
|
|
741
|
-
try {
|
|
742
|
-
response = await http({
|
|
743
|
-
method: 'GET',
|
|
744
|
-
url: imageUrl,
|
|
745
|
-
responseType: 'stream',
|
|
746
|
-
timeout: config.imageDownloadTimeout || 60000,
|
|
779
|
+
timeout,
|
|
747
780
|
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
|
|
748
781
|
maxRedirects: 5,
|
|
749
782
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
@@ -752,14 +785,14 @@ function apply(ctx, config) {
|
|
|
752
785
|
catch (e) {
|
|
753
786
|
writer.destroy();
|
|
754
787
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
755
|
-
throw new Error(
|
|
788
|
+
throw new Error(`下载失败: ${getErrorMessage(e)}`);
|
|
756
789
|
}
|
|
757
|
-
const maxSizeBytes =
|
|
790
|
+
const maxSizeBytes = maxSize * 1024 * 1024;
|
|
758
791
|
const contentLength = Number(response.headers['content-length'] || 0);
|
|
759
792
|
if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
|
|
760
793
|
writer.destroy();
|
|
761
794
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
762
|
-
throw new Error(
|
|
795
|
+
throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
|
|
763
796
|
}
|
|
764
797
|
try {
|
|
765
798
|
await (0, promises_2.pipeline)(response.data, writer);
|
|
@@ -767,83 +800,68 @@ function apply(ctx, config) {
|
|
|
767
800
|
}
|
|
768
801
|
catch (e) {
|
|
769
802
|
await promises_1.default.unlink(filePath).catch(() => { });
|
|
770
|
-
throw new Error(
|
|
803
|
+
throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
|
|
771
804
|
}
|
|
772
805
|
}
|
|
773
|
-
async function
|
|
774
|
-
if (!
|
|
806
|
+
async function sendMedia(session, url, type, forceDownload, showFile, timeout, tempDir, maxSize) {
|
|
807
|
+
if (!url)
|
|
775
808
|
return;
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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) {
|
|
785
820
|
try {
|
|
786
|
-
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
|
+
}
|
|
787
828
|
return;
|
|
788
829
|
}
|
|
789
|
-
catch {
|
|
790
|
-
|
|
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
|
+
}
|
|
791
838
|
}
|
|
839
|
+
return;
|
|
792
840
|
}
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
if (!config.showImageFile) {
|
|
796
|
-
await sendLink();
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
try {
|
|
800
|
-
await sendWithTimeout(session, koishi_1.h.image(imageUrl));
|
|
801
|
-
}
|
|
802
|
-
catch {
|
|
803
|
-
try {
|
|
804
|
-
const localPath = await downloadImageFile(imageUrl);
|
|
805
|
-
await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
|
|
806
|
-
}
|
|
807
|
-
catch {
|
|
841
|
+
if (!showFile) {
|
|
808
842
|
await sendLink();
|
|
843
|
+
return;
|
|
809
844
|
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
async function sendVideoFile(session, videoUrl) {
|
|
813
|
-
if (!videoUrl)
|
|
814
|
-
return;
|
|
815
|
-
if (!config.showVideoFile)
|
|
816
|
-
return await sendWithTimeout(session, `视频链接:${videoUrl}`);
|
|
817
|
-
const sendLink = async () => { await sendWithTimeout(session, `视频链接:${videoUrl}`).catch(() => { }); };
|
|
818
|
-
if (config.forceDownloadVideo) {
|
|
819
845
|
try {
|
|
820
|
-
|
|
821
|
-
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
822
|
-
return;
|
|
846
|
+
await sendWithTimeout(session, sendFunc(url));
|
|
823
847
|
}
|
|
824
|
-
catch
|
|
825
|
-
debugLog('ERROR', '强制下载视频失败,尝试URL发送:', getErrorMessage(e));
|
|
848
|
+
catch {
|
|
826
849
|
try {
|
|
827
|
-
await
|
|
828
|
-
|
|
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
|
+
}
|
|
829
857
|
}
|
|
830
858
|
catch {
|
|
831
859
|
await sendLink();
|
|
832
860
|
}
|
|
833
861
|
}
|
|
834
|
-
return;
|
|
835
862
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
catch {
|
|
840
|
-
try {
|
|
841
|
-
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
842
|
-
await sendWithTimeout(session, koishi_1.h.video(`file://${tempFilePath}`));
|
|
843
|
-
}
|
|
844
|
-
catch {
|
|
845
|
-
await sendLink();
|
|
846
|
-
}
|
|
863
|
+
finally {
|
|
864
|
+
downloadLimiter.release();
|
|
847
865
|
}
|
|
848
866
|
}
|
|
849
867
|
async function flush(session, matches) {
|
|
@@ -854,6 +872,11 @@ function apply(ctx, config) {
|
|
|
854
872
|
const promises = matches.map(async (match) => {
|
|
855
873
|
await limiter.acquire();
|
|
856
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
|
+
}
|
|
857
880
|
if (config.deduplicationInterval > 0) {
|
|
858
881
|
const lastTime = dedupCache.get(match.url);
|
|
859
882
|
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
@@ -864,8 +887,9 @@ function apply(ctx, config) {
|
|
|
864
887
|
}
|
|
865
888
|
}
|
|
866
889
|
debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
|
|
867
|
-
const
|
|
868
|
-
const
|
|
890
|
+
const platformConf = getPlatformConfig(match.type);
|
|
891
|
+
const fieldMapping = platformConf.fieldMapping;
|
|
892
|
+
const result = await processSingleUrl(match.url, match.type, fieldMapping, platformConf);
|
|
869
893
|
if (result.success) {
|
|
870
894
|
items.push(result.data);
|
|
871
895
|
if (config.deduplicationInterval > 0)
|
|
@@ -885,7 +909,7 @@ function apply(ctx, config) {
|
|
|
885
909
|
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`);
|
|
886
910
|
if (!items.length)
|
|
887
911
|
return;
|
|
888
|
-
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
912
|
+
const enableForward = config.enableForward && (session.platform === 'onebot' || session.platform === 'satori');
|
|
889
913
|
const botName = config.botName || '视频解析机器人';
|
|
890
914
|
if (enableForward) {
|
|
891
915
|
const forwardMessages = [];
|
|
@@ -894,9 +918,12 @@ function apply(ctx, config) {
|
|
|
894
918
|
const text = item.text;
|
|
895
919
|
if (text && config.showImageText)
|
|
896
920
|
forwardMessages.push(buildForwardNode(session, text, botName));
|
|
897
|
-
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
921
|
+
if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
898
922
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
899
923
|
}
|
|
924
|
+
if (config.showMusicCover && p.music.cover) {
|
|
925
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.music.cover), botName));
|
|
926
|
+
}
|
|
900
927
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
901
928
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
902
929
|
for (const imgUrl of imageUrls)
|
|
@@ -904,6 +931,9 @@ function apply(ctx, config) {
|
|
|
904
931
|
}
|
|
905
932
|
if (p.video)
|
|
906
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
|
+
}
|
|
907
937
|
}
|
|
908
938
|
if (forwardMessages.length) {
|
|
909
939
|
try {
|
|
@@ -926,34 +956,42 @@ function apply(ctx, config) {
|
|
|
926
956
|
await sendWithTimeout(session, text);
|
|
927
957
|
await delay(300);
|
|
928
958
|
}
|
|
929
|
-
if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
930
|
-
await
|
|
959
|
+
if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
960
|
+
await sendMedia(session, p.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
|
|
961
|
+
await delay(300);
|
|
962
|
+
}
|
|
963
|
+
if (config.showMusicCover && p.music.cover) {
|
|
964
|
+
await sendMedia(session, p.music.cover, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
|
|
931
965
|
await delay(300);
|
|
932
966
|
}
|
|
933
967
|
if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
|
|
934
|
-
await
|
|
968
|
+
await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile, config.videoDownloadTimeout, config.tempDir, config.maxVideoSize).catch(() => { });
|
|
935
969
|
await delay(500);
|
|
936
970
|
}
|
|
937
971
|
if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
|
|
938
972
|
const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
|
|
939
973
|
for (const imgUrl of imageUrls) {
|
|
940
|
-
await
|
|
974
|
+
await sendMedia(session, imgUrl, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
|
|
941
975
|
await delay(200);
|
|
942
976
|
}
|
|
943
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
|
+
}
|
|
944
982
|
}
|
|
945
983
|
}
|
|
946
984
|
debugLog('INFO', '处理完成');
|
|
947
985
|
}
|
|
948
|
-
async function fetchApi(url, type, fieldMapping) {
|
|
986
|
+
async function fetchApi(url, type, fieldMapping, platformConf) {
|
|
949
987
|
const cacheKey = url;
|
|
950
988
|
const cached = urlCacheLocal.get(cacheKey);
|
|
951
989
|
if (cached && cached.expire > Date.now())
|
|
952
990
|
return cached.data;
|
|
953
|
-
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
|
|
991
|
+
const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, customProxy } = platformConf || getPlatformConfig(type);
|
|
954
992
|
const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
|
|
955
993
|
const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
|
|
956
|
-
const backupAllowed =
|
|
994
|
+
const backupAllowed = new Set(['douyin', 'xiaohongshu', 'instagram', 'jimeng']).has(type);
|
|
957
995
|
const apiList = [];
|
|
958
996
|
if (dedicatedFirst && dedicatedUrl) {
|
|
959
997
|
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
@@ -968,6 +1006,9 @@ function apply(ctx, config) {
|
|
|
968
1006
|
if (dedicatedUrl)
|
|
969
1007
|
apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
970
1008
|
}
|
|
1009
|
+
if (type.startsWith('custom_') && apiList.length === 0 && dedicatedUrl) {
|
|
1010
|
+
apiList.push({ url: dedicatedUrl, label: `自定义API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
|
|
1011
|
+
}
|
|
971
1012
|
const customHeaders = config.customHeaders || [];
|
|
972
1013
|
let lastError = null;
|
|
973
1014
|
for (const api of apiList) {
|
|
@@ -986,7 +1027,19 @@ function apply(ctx, config) {
|
|
|
986
1027
|
const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
|
|
987
1028
|
Object.assign(headers, authHeaders);
|
|
988
1029
|
}
|
|
989
|
-
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);
|
|
990
1043
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
991
1044
|
const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
|
|
992
1045
|
urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
|
|
@@ -1005,12 +1058,12 @@ function apply(ctx, config) {
|
|
|
1005
1058
|
}
|
|
1006
1059
|
throw lastError || new Error('所有API请求全部失败');
|
|
1007
1060
|
}
|
|
1008
|
-
async function parseUrl(url, type, fieldMapping) {
|
|
1061
|
+
async function parseUrl(url, type, fieldMapping, platformConf) {
|
|
1009
1062
|
const realUrl = await resolveShortUrl(url);
|
|
1010
1063
|
const candidates = [...new Set([realUrl, url])];
|
|
1011
1064
|
for (const candidate of candidates) {
|
|
1012
1065
|
try {
|
|
1013
|
-
const info = await fetchApi(candidate, type, fieldMapping);
|
|
1066
|
+
const info = await fetchApi(candidate, type, fieldMapping, platformConf);
|
|
1014
1067
|
if (info.video || info.images.length > 0 || info.live_photo.length > 0)
|
|
1015
1068
|
return { success: true, data: info };
|
|
1016
1069
|
debugLog('WARN', `解析成功但无内容: ${candidate}`);
|
|
@@ -1021,8 +1074,8 @@ function apply(ctx, config) {
|
|
|
1021
1074
|
}
|
|
1022
1075
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
1023
1076
|
}
|
|
1024
|
-
async function processSingleUrl(url, type, fieldMapping) {
|
|
1025
|
-
const result = await parseUrl(url, type, fieldMapping);
|
|
1077
|
+
async function processSingleUrl(url, type, fieldMapping, platformConf) {
|
|
1078
|
+
const result = await parseUrl(url, type, fieldMapping, platformConf);
|
|
1026
1079
|
if (!result.success)
|
|
1027
1080
|
return { success: false, msg: result.msg, url };
|
|
1028
1081
|
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
@@ -1053,6 +1106,27 @@ function apply(ctx, config) {
|
|
|
1053
1106
|
}
|
|
1054
1107
|
return null;
|
|
1055
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);
|
|
1056
1130
|
ctx.on('message', async (session) => {
|
|
1057
1131
|
if (!config.enable)
|
|
1058
1132
|
return;
|
|
@@ -1062,7 +1136,7 @@ function apply(ctx, config) {
|
|
|
1062
1136
|
return;
|
|
1063
1137
|
if (session.selfId === session.userId)
|
|
1064
1138
|
return;
|
|
1065
|
-
const matches = extractAllUrlsFromMessage(session);
|
|
1139
|
+
const matches = extractAllUrlsFromMessage(session, customRules);
|
|
1066
1140
|
if (!matches.length)
|
|
1067
1141
|
return;
|
|
1068
1142
|
debugLog('INFO', `检测到 ${matches.length} 个链接`);
|
|
@@ -1081,7 +1155,7 @@ function apply(ctx, config) {
|
|
|
1081
1155
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
1082
1156
|
return;
|
|
1083
1157
|
}
|
|
1084
|
-
const matches = linkTypeParser(url);
|
|
1158
|
+
const matches = linkTypeParser(url, customRules);
|
|
1085
1159
|
if (!matches.length) {
|
|
1086
1160
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
1087
1161
|
return;
|
|
@@ -1096,12 +1170,14 @@ function apply(ctx, config) {
|
|
|
1096
1170
|
});
|
|
1097
1171
|
const tempCleanupInterval = setInterval(async () => {
|
|
1098
1172
|
try {
|
|
1099
|
-
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'];
|
|
1100
1174
|
for (const dir of dirs) {
|
|
1101
1175
|
const files = await promises_1.default.readdir(dir);
|
|
1102
1176
|
const now = Date.now();
|
|
1103
1177
|
for (const file of files) {
|
|
1104
|
-
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))) {
|
|
1105
1181
|
const filePath = path_1.default.join(dir, file);
|
|
1106
1182
|
const stats = await promises_1.default.stat(filePath);
|
|
1107
1183
|
if (now - stats.mtimeMs > 3600000) {
|
|
@@ -1123,11 +1199,13 @@ function apply(ctx, config) {
|
|
|
1123
1199
|
});
|
|
1124
1200
|
process.on('beforeExit', async () => {
|
|
1125
1201
|
try {
|
|
1126
|
-
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'];
|
|
1127
1203
|
for (const dir of dirs) {
|
|
1128
1204
|
const files = await promises_1.default.readdir(dir);
|
|
1129
1205
|
for (const file of files) {
|
|
1130
|
-
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))) {
|
|
1131
1209
|
await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
|
|
1132
1210
|
}
|
|
1133
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,17 +27,19 @@ 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
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
38
39
|
|--------|------|--------|------|
|
|
39
40
|
| `showImageText` | boolean | true | 是否发送文字内容 |
|
|
40
|
-
| `showCoverImage` | boolean | true |
|
|
41
|
+
| `showCoverImage` | boolean | true | 是否发送封面图片(视频/图集封面) |
|
|
42
|
+
| `showMusicCover` | boolean | true | 是否发送音乐封面图片 |
|
|
41
43
|
| `showImageFile` | boolean | true | 封面/图片是否以文件形式发送(关闭则只发链接) |
|
|
42
44
|
| `forceDownloadImage` | boolean | false | 强制下载封面/图片后发送 |
|
|
43
45
|
| `imageDownloadTimeout` | number | 60000 | 图片下载超时(毫秒) |
|
|
@@ -49,7 +51,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
49
51
|
| `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
|
|
50
52
|
| `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 不限制 |
|
|
51
53
|
| `maxDescLength` | number | 200 | 简介最大长度(字符) |
|
|
52
|
-
| `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 不限制 |
|
|
53
62
|
|
|
54
63
|
### 网络与 API 设置 (Network & API Settings)
|
|
55
64
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -63,10 +72,9 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
63
72
|
### API 选择与回退设置 (API Selection & Fallback)
|
|
64
73
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
65
74
|
|--------|------|--------|------|
|
|
66
|
-
| `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址 |
|
|
67
|
-
| `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API,仅支持部分平台 |
|
|
68
75
|
| `platformDedicatedFirst` | object | 各平台均为 `false` | 平台专属 API 优先开关,键:`bilibili` 等 |
|
|
69
|
-
| `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`(独立代理) |
|
|
70
78
|
| `globalFieldMapping` | string | 预设字段映射 JSON | 全局字段映射,支持点号路径 |
|
|
71
79
|
|
|
72
80
|
### 错误与重试设置 (Error & Retry Settings)
|
|
@@ -79,7 +87,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
79
87
|
### 发送方式设置 (Send Mode Settings)
|
|
80
88
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
81
89
|
|--------|------|--------|------|
|
|
82
|
-
| `enableForward` | boolean | false |
|
|
90
|
+
| `enableForward` | boolean | false | 启用合并转发(支持 OneBot、Satori 平台) |
|
|
83
91
|
|
|
84
92
|
### 缓存与去重设置 (Cache & Deduplication Settings)
|
|
85
93
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -116,10 +124,14 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
116
124
|
| `${视频链接}` | 视频原始链接 |
|
|
117
125
|
| `${音乐标题}` | 音乐标题 |
|
|
118
126
|
| `${音乐作者}` | 音乐作者 |
|
|
119
|
-
| `${音乐封面}` | 音乐封面图片地址 |
|
|
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,18 +162,19 @@ 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
|
|
|
158
171
|
| 贡献者 (Contributor) | 贡献内容 (Contribution) |
|
|
159
172
|
|----------------------|-------------------------|
|
|
160
|
-
| Minecraft-1314 | 插件完整开发 |
|
|
161
|
-
| ShiraiKuroko003 |
|
|
162
|
-
| cyavb |
|
|
163
|
-
| Keep785 |
|
|
164
|
-
| dzt2008 + Apricityx |
|
|
173
|
+
| Minecraft-1314 | 插件完整开发 (Complete plugin development) |
|
|
174
|
+
| ShiraiKuroko003 | 修复消息格式设置问题并且PR-1.2.5版本已修复 |
|
|
175
|
+
| cyavb | 提交功能建议-给自定义API添加KEY认证-已修复 |
|
|
176
|
+
| Keep785 | 提交Bug-无法正常关闭发送封面-已修复 |
|
|
177
|
+
| dzt2008 + Apricityx | 提交Bug-会对非支持视频平台URL进行误解析-已修复 |
|
|
165
178
|
| JH-Ahua | BugPk-Api 支持 |
|
|
166
179
|
| shangxue | 灵感来源 |
|
|
167
180
|
|