koishi-plugin-video-parser-all 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import { Context, Schema } from 'koishi';
2
+ declare module 'koishi' {
3
+ interface Context {
4
+ downloads?: {
5
+ download(url: string, dest: string, options?: Record<string, unknown>): Promise<string>;
6
+ };
7
+ }
8
+ }
2
9
  export declare const name = "video-parser-all";
3
10
  export declare const Config: Schema<{
4
11
  enable?: boolean | null | undefined;
@@ -23,9 +30,13 @@ export declare const Config: Schema<{
23
30
  twitter?: boolean | null | undefined;
24
31
  instagram?: boolean | null | undefined;
25
32
  doubao?: boolean | null | undefined;
26
- doubao_chat?: boolean | null | undefined;
27
33
  oasis?: boolean | null | undefined;
28
34
  wechat_channel?: boolean | null | undefined;
35
+ lishi?: boolean | null | undefined;
36
+ quanmin?: boolean | null | undefined;
37
+ pipigx?: boolean | null | undefined;
38
+ pipixia?: boolean | null | undefined;
39
+ zuiyou?: boolean | null | undefined;
29
40
  } & import("cosmokit").Dict) | null | undefined;
30
41
  } & import("cosmokit").Dict & {
31
42
  unifiedMessageFormat?: string | null | undefined;
@@ -34,24 +45,24 @@ export declare const Config: Schema<{
34
45
  showCoverImage?: boolean | null | undefined;
35
46
  showMusicCover?: boolean | null | undefined;
36
47
  showImageFile?: boolean | null | undefined;
37
- forceDownloadImage?: boolean | null | undefined;
38
- imageDownloadTimeout?: number | null | undefined;
39
- imageTempDir?: string | null | undefined;
40
- maxImageSize?: number | null | undefined;
41
48
  showVideoFile?: boolean | null | undefined;
49
+ forceDownloadImage?: boolean | null | undefined;
42
50
  forceDownloadVideo?: boolean | null | undefined;
43
- videoDownloadTimeout?: number | null | undefined;
44
- tempDir?: string | null | undefined;
45
- maxVideoSize?: number | null | undefined;
46
- maxDescLength?: number | null | undefined;
47
- maxConcurrent?: number | null | undefined;
48
- downloadConcurrency?: number | null | undefined;
51
+ } & {
49
52
  showMusicVoice?: boolean | null | undefined;
50
53
  showMusicVoiceFile?: boolean | null | undefined;
51
54
  forceDownloadMusicVoice?: boolean | null | undefined;
52
- musicDownloadTimeout?: number | null | undefined;
53
- musicTempDir?: string | null | undefined;
54
- maxMusicSize?: number | null | undefined;
55
+ } & {
56
+ maxDescLength?: number | null | undefined;
57
+ maxConcurrent?: number | null | undefined;
58
+ downloadConcurrency?: number | null | undefined;
59
+ mediaDownloadTimeout?: number | null | undefined;
60
+ maxMediaSize?: number | null | undefined;
61
+ downloadEngine?: "internal" | "aria2" | null | undefined;
62
+ aria2Host?: string | null | undefined;
63
+ aria2Port?: number | null | undefined;
64
+ aria2Secret?: string | null | undefined;
65
+ resumeDownload?: boolean | null | undefined;
55
66
  } & {
56
67
  timeout?: number | null | undefined;
57
68
  videoSendTimeout?: number | null | undefined;
@@ -74,14 +85,19 @@ export declare const Config: Schema<{
74
85
  ignoreSendError?: boolean | null | undefined;
75
86
  retryTimes?: number | null | undefined;
76
87
  retryInterval?: number | null | undefined;
77
- } & {
78
88
  enableForward?: boolean | null | undefined;
79
89
  } & {
80
90
  deduplicationInterval?: number | null | undefined;
81
91
  cacheTTL?: number | null | undefined;
92
+ cacheDir?: string | null | undefined;
82
93
  } & {
83
94
  primaryApiUrl?: string | null | undefined;
84
95
  backupApiUrl?: string | null | undefined;
96
+ apiKeys?: ({
97
+ key?: string | null | undefined;
98
+ weight?: number | null | undefined;
99
+ } & import("cosmokit").Dict)[] | null | undefined;
100
+ rotationMode?: "sequential" | "load_balance" | null | undefined;
85
101
  platformDedicatedFirst?: ({
86
102
  bilibili?: boolean | null | undefined;
87
103
  douyin?: boolean | null | undefined;
@@ -100,12 +116,16 @@ export declare const Config: Schema<{
100
116
  twitter?: boolean | null | undefined;
101
117
  instagram?: boolean | null | undefined;
102
118
  doubao?: boolean | null | undefined;
103
- doubao_chat?: boolean | null | undefined;
104
119
  oasis?: boolean | null | undefined;
105
120
  wechat_channel?: boolean | null | undefined;
121
+ lishi?: boolean | null | undefined;
122
+ quanmin?: boolean | null | undefined;
123
+ pipigx?: boolean | null | undefined;
124
+ pipixia?: boolean | null | undefined;
125
+ zuiyou?: boolean | null | undefined;
106
126
  } & import("cosmokit").Dict) | null | undefined;
107
127
  customApis?: ({
108
- platform?: "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "doubao_chat" | "oasis" | "wechat_channel" | null | undefined;
128
+ platform?: "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel" | null | undefined;
109
129
  apiUrl?: string | null | undefined;
110
130
  apiKey?: string | null | undefined;
111
131
  authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
@@ -162,9 +182,13 @@ export declare const Config: Schema<{
162
182
  twitter: Schema<boolean, boolean>;
163
183
  instagram: Schema<boolean, boolean>;
164
184
  doubao: Schema<boolean, boolean>;
165
- doubao_chat: Schema<boolean, boolean>;
166
185
  oasis: Schema<boolean, boolean>;
167
186
  wechat_channel: Schema<boolean, boolean>;
187
+ lishi: Schema<boolean, boolean>;
188
+ quanmin: Schema<boolean, boolean>;
189
+ pipigx: Schema<boolean, boolean>;
190
+ pipixia: Schema<boolean, boolean>;
191
+ zuiyou: Schema<boolean, boolean>;
168
192
  }>;
169
193
  } & import("cosmokit").Dict & {
170
194
  unifiedMessageFormat: string;
@@ -173,24 +197,24 @@ export declare const Config: Schema<{
173
197
  showCoverImage: boolean;
174
198
  showMusicCover: boolean;
175
199
  showImageFile: boolean;
176
- forceDownloadImage: boolean;
177
- imageDownloadTimeout: number;
178
- imageTempDir: string;
179
- maxImageSize: number;
180
200
  showVideoFile: boolean;
201
+ forceDownloadImage: boolean;
181
202
  forceDownloadVideo: boolean;
182
- videoDownloadTimeout: number;
183
- tempDir: string;
184
- maxVideoSize: number;
185
- maxDescLength: number;
186
- maxConcurrent: number;
187
- downloadConcurrency: number;
203
+ } & {
188
204
  showMusicVoice: boolean;
189
205
  showMusicVoiceFile: boolean;
190
206
  forceDownloadMusicVoice: boolean;
191
- musicDownloadTimeout: number;
192
- musicTempDir: string;
193
- maxMusicSize: number;
207
+ } & {
208
+ maxDescLength: number;
209
+ maxConcurrent: number;
210
+ downloadConcurrency: number;
211
+ mediaDownloadTimeout: number;
212
+ maxMediaSize: number;
213
+ downloadEngine: "internal" | "aria2";
214
+ aria2Host: string;
215
+ aria2Port: number;
216
+ aria2Secret: string;
217
+ resumeDownload: boolean;
194
218
  } & {
195
219
  timeout: number;
196
220
  videoSendTimeout: number;
@@ -216,14 +240,19 @@ export declare const Config: Schema<{
216
240
  ignoreSendError: boolean;
217
241
  retryTimes: number;
218
242
  retryInterval: number;
219
- } & {
220
243
  enableForward: boolean;
221
244
  } & {
222
245
  deduplicationInterval: number;
223
246
  cacheTTL: number;
247
+ cacheDir: string;
224
248
  } & {
225
249
  primaryApiUrl: string;
226
250
  backupApiUrl: string;
251
+ apiKeys: Schemastery.ObjectT<{
252
+ key: Schema<string, string>;
253
+ weight: Schema<number, number>;
254
+ }>[];
255
+ rotationMode: "sequential" | "load_balance";
227
256
  platformDedicatedFirst: Schemastery.ObjectT<{
228
257
  bilibili: Schema<boolean, boolean>;
229
258
  douyin: Schema<boolean, boolean>;
@@ -242,12 +271,16 @@ export declare const Config: Schema<{
242
271
  twitter: Schema<boolean, boolean>;
243
272
  instagram: Schema<boolean, boolean>;
244
273
  doubao: Schema<boolean, boolean>;
245
- doubao_chat: Schema<boolean, boolean>;
246
274
  oasis: Schema<boolean, boolean>;
247
275
  wechat_channel: Schema<boolean, boolean>;
276
+ lishi: Schema<boolean, boolean>;
277
+ quanmin: Schema<boolean, boolean>;
278
+ pipigx: Schema<boolean, boolean>;
279
+ pipixia: Schema<boolean, boolean>;
280
+ zuiyou: Schema<boolean, boolean>;
248
281
  }>;
249
282
  customApis: Schemastery.ObjectT<{
250
- platform: Schema<"bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "doubao_chat" | "oasis" | "wechat_channel", "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "doubao_chat" | "oasis" | "wechat_channel">;
283
+ platform: Schema<"bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel", "bilibili" | "douyin" | "kuaishou" | "xiaohongshu" | "weibo" | "xigua" | "youtube" | "tiktok" | "acfun" | "zhihu" | "weishi" | "huya" | "haokan" | "meipai" | "twitter" | "instagram" | "doubao" | "oasis" | "wechat_channel">;
251
284
  apiUrl: Schema<string, string>;
252
285
  apiKey: Schema<string, string>;
253
286
  authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
package/lib/index.js CHANGED
@@ -71,9 +71,9 @@ exports.name = 'video-parser-all';
71
71
  exports.Config = koishi_1.Schema.intersect([
72
72
  koishi_1.Schema.object({
73
73
  enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
74
- botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发消息中显示的机器人名称'),
74
+ botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发中显示的昵称'),
75
75
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
76
- debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
76
+ debug: koishi_1.Schema.boolean().default(false).description('开启调试日志'),
77
77
  platformEnabled: koishi_1.Schema.object({
78
78
  bilibili: koishi_1.Schema.boolean().default(true).description('哔哩哔哩'),
79
79
  douyin: koishi_1.Schema.boolean().default(true).description('抖音'),
@@ -92,41 +92,50 @@ exports.Config = koishi_1.Schema.intersect([
92
92
  twitter: koishi_1.Schema.boolean().default(true).description('Twitter/X'),
93
93
  instagram: koishi_1.Schema.boolean().default(true).description('Instagram'),
94
94
  doubao: koishi_1.Schema.boolean().default(true).description('豆包'),
95
- doubao_chat: koishi_1.Schema.boolean().default(true).description('豆包对话'),
96
95
  oasis: koishi_1.Schema.boolean().default(true).description('绿洲'),
97
96
  wechat_channel: koishi_1.Schema.boolean().default(true).description('视频号'),
97
+ lishi: koishi_1.Schema.boolean().default(true).description('梨视频'),
98
+ quanmin: koishi_1.Schema.boolean().default(true).description('全民直播'),
99
+ pipigx: koishi_1.Schema.boolean().default(true).description('皮皮搞笑'),
100
+ pipixia: koishi_1.Schema.boolean().default(true).description('皮皮虾'),
101
+ zuiyou: koishi_1.Schema.boolean().default(true).description('最右'),
98
102
  }).description('各平台解析开关'),
99
- }).description('基础设置'),
103
+ }).description('基本设置'),
100
104
  koishi_1.Schema.object({
101
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('文字消息格式,支持变量。某行所有变量为空时自动隐藏。封面及媒体文件由独立开关控制,默认不包含在文字中'),
102
- }).description('消息格式设置'),
105
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('文字格式,支持变量,空行自动隐藏'),
106
+ }).description('消息格式'),
103
107
  koishi_1.Schema.object({
104
- showImageText: 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('是否发送音乐封面图片'),
107
- showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以文件形式发送(关闭则只发送链接)'),
108
- forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片后发送'),
109
- imageDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('图片下载超时(毫秒)'),
110
- imageTempDir: koishi_1.Schema.string().default('./temp_images').description('临时封面/图片存储目录'),
111
- maxImageSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载图片大小(MB),0 为不限制'),
112
- showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以文件形式发送(关闭则只发送链接)'),
113
- forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频后发送'),
114
- videoDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('视频下载超时(毫秒)'),
115
- tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
116
- maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
117
- maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介最大长度(字符)'),
108
+ showImageText: koishi_1.Schema.boolean().default(true).description('发送文字内容'),
109
+ showCoverImage: koishi_1.Schema.boolean().default(true).description('发送封面图片'),
110
+ showMusicCover: koishi_1.Schema.boolean().default(true).description('发送音乐封面图片'),
111
+ showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以图片形式发送(关闭则只发送链接)'),
112
+ showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以视频形式发送(关闭则只发送链接)'),
113
+ forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片'),
114
+ forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频'),
115
+ }).description('媒体发送'),
116
+ koishi_1.Schema.object({
117
+ showMusicVoice: koishi_1.Schema.boolean().default(false).description('音乐链接以语音形式发送'),
118
+ showMusicVoiceFile: koishi_1.Schema.boolean().default(true).description('音乐语音是否以文件形式发送(关闭则只发送链接)'),
119
+ forceDownloadMusicVoice: koishi_1.Schema.boolean().default(false).description('强制下载音乐语音'),
120
+ }).description('音乐语音(需 silk 和 ffmpeg)'),
121
+ koishi_1.Schema.object({
122
+ maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介长度上限'),
118
123
  maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('解析最大并发数'),
119
124
  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 为不限制'),
126
- }).description('内容显示设置'),
125
+ mediaDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('统一下载超时 (ms)'),
126
+ maxMediaSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载文件大小 (MB),0 为不限制'),
127
+ downloadEngine: koishi_1.Schema.union([
128
+ koishi_1.Schema.const('internal').description('内置下载'),
129
+ koishi_1.Schema.const('aria2').description('aria2 下载'),
130
+ ]).default('internal').description('下载引擎'),
131
+ aria2Host: koishi_1.Schema.string().default('127.0.0.1').description('aria2 RPC 地址'),
132
+ aria2Port: koishi_1.Schema.number().default(6800).description('aria2 RPC 端口'),
133
+ aria2Secret: koishi_1.Schema.string().default('').description('aria2 RPC 密钥'),
134
+ resumeDownload: koishi_1.Schema.boolean().default(true).description('启用断点续传(仅 aria2 模式)'),
135
+ }).description('性能与限制'),
127
136
  koishi_1.Schema.object({
128
- timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
129
- videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('消息发送超时(毫秒)'),
137
+ timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时 (ms)'),
138
+ videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('消息发送超时 (ms)'),
130
139
  userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36').description('User-Agent'),
131
140
  proxy: koishi_1.Schema.object({
132
141
  enabled: koishi_1.Schema.boolean().default(false).description('启用代理'),
@@ -145,22 +154,29 @@ exports.Config = koishi_1.Schema.intersect([
145
154
  name: koishi_1.Schema.string().required().description('头名称'),
146
155
  value: koishi_1.Schema.string().required().description('头值'),
147
156
  })).default([]).description('自定义请求头'),
148
- }).description('网络设置'),
157
+ }).description('网络与请求'),
149
158
  koishi_1.Schema.object({
150
159
  ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败'),
151
160
  retryTimes: koishi_1.Schema.number().min(0).step(1).default(3).description('重试次数'),
152
- retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
153
- }).description('错误与重试'),
154
- koishi_1.Schema.object({
155
- enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(支持 OneBot、Satori 平台)'),
156
- }).description('发送方式'),
161
+ retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔 (ms)'),
162
+ enableForward: koishi_1.Schema.boolean().default(false).description('合并转发(OneBot/Satori)'),
163
+ }).description('发送与重试'),
157
164
  koishi_1.Schema.object({
158
- deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
159
- cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
160
- }).description('缓存与去重'),
165
+ deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔 (s)'),
166
+ cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间 (s)'),
167
+ cacheDir: koishi_1.Schema.string().default('./temp_cache').description('统一临时目录'),
168
+ }).description('缓存与临时文件'),
161
169
  koishi_1.Schema.object({
162
170
  primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').hidden(),
163
171
  backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').hidden(),
172
+ apiKeys: koishi_1.Schema.array(koishi_1.Schema.object({
173
+ key: koishi_1.Schema.string().required().description('API Key'),
174
+ weight: koishi_1.Schema.number().min(1).default(1).description('权重(负载均衡模式)'),
175
+ })).default([]).description('多 API 密钥(轮换使用)'),
176
+ rotationMode: koishi_1.Schema.union([
177
+ koishi_1.Schema.const('sequential').description('顺序模式(无效时切换)'),
178
+ koishi_1.Schema.const('load_balance').description('负载均衡模式(轮询)'),
179
+ ]).default('sequential').description('密钥轮换模式'),
164
180
  platformDedicatedFirst: koishi_1.Schema.object({
165
181
  bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
166
182
  douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
@@ -179,9 +195,13 @@ exports.Config = koishi_1.Schema.intersect([
179
195
  twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X'),
180
196
  instagram: koishi_1.Schema.boolean().default(false).description('Instagram'),
181
197
  doubao: koishi_1.Schema.boolean().default(false).description('豆包'),
182
- doubao_chat: koishi_1.Schema.boolean().default(false).description('豆包对话'),
183
198
  oasis: koishi_1.Schema.boolean().default(false).description('绿洲'),
184
199
  wechat_channel: koishi_1.Schema.boolean().default(false).description('视频号'),
200
+ lishi: koishi_1.Schema.boolean().default(false).description('梨视频'),
201
+ quanmin: koishi_1.Schema.boolean().default(false).description('全民直播'),
202
+ pipigx: koishi_1.Schema.boolean().default(false).description('皮皮搞笑'),
203
+ pipixia: koishi_1.Schema.boolean().default(false).description('皮皮虾'),
204
+ zuiyou: koishi_1.Schema.boolean().default(false).description('最右'),
185
205
  }).description('优先使用专属 API'),
186
206
  customApis: koishi_1.Schema.array(koishi_1.Schema.object({
187
207
  platform: koishi_1.Schema.union([
@@ -202,7 +222,6 @@ exports.Config = koishi_1.Schema.intersect([
202
222
  koishi_1.Schema.const('twitter').description('Twitter/X'),
203
223
  koishi_1.Schema.const('instagram').description('Instagram'),
204
224
  koishi_1.Schema.const('doubao').description('豆包'),
205
- koishi_1.Schema.const('doubao_chat').description('豆包对话'),
206
225
  koishi_1.Schema.const('oasis').description('绿洲'),
207
226
  koishi_1.Schema.const('wechat_channel').description('视频号'),
208
227
  ]).description('平台'),
@@ -215,12 +234,12 @@ exports.Config = koishi_1.Schema.intersect([
215
234
  ]).default('Bearer').description('认证头类型'),
216
235
  customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
217
236
  fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
218
- })).default([]).description('自定义内置平台专属 API'),
237
+ })).default([]).description('覆盖内置平台 API'),
219
238
  customPlatforms: koishi_1.Schema.array(koishi_1.Schema.object({
220
239
  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 地址'),
240
+ exampleUrl: koishi_1.Schema.string().description('示例链接'),
241
+ keywords: koishi_1.Schema.string().required().description('关键词(逗号分隔)'),
242
+ apiUrl: koishi_1.Schema.string().required().description('解析 API'),
224
243
  apiKey: koishi_1.Schema.string().default('').description('API Key'),
225
244
  authHeaderType: koishi_1.Schema.union([
226
245
  koishi_1.Schema.const('Bearer').description('Bearer'),
@@ -266,14 +285,14 @@ exports.Config = koishi_1.Schema.intersect([
266
285
  ' "music_cover": "data.music.cover",\n' +
267
286
  ' "music_url": "data.music.url"\n' +
268
287
  '}').description('全局字段映射 JSON'),
269
- }).description('API 选择'),
288
+ }).description('API 与平台'),
270
289
  koishi_1.Schema.object({
271
290
  waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示'),
272
291
  unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持提示'),
273
292
  invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示'),
274
293
  parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('错误前缀'),
275
- parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('错误项格式'),
276
- }).description('界面文字'),
294
+ parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('错误格式'),
295
+ }).description('界面文本'),
277
296
  ]);
278
297
  const logger = new koishi_1.Logger(exports.name);
279
298
  let debugEnabled = false;
@@ -309,10 +328,19 @@ const BUILTIN_LINK_RULES = [
309
328
  { pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
310
329
  { pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
311
330
  { pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
312
- { pattern: /https?:\/\/(?:www\.)?doubao\.com\/thread\/[0-9a-zA-Z_-]+/gi, type: 'doubao_chat' },
313
331
  { pattern: /https?:\/\/(?:www\.)?oasis\.weibo\.com\/v\/[0-9a-zA-Z_-]+/gi, type: 'oasis' },
314
332
  { pattern: /https?:\/\/channels\.weixin\.qq\.com\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
315
333
  { pattern: /https?:\/\/weixin\.qq\.com\/sph\/[0-9a-zA-Z_-]+/gi, type: 'wechat_channel' },
334
+ { pattern: /https?:\/\/(?:www\.)?pearvideo\.com\/video_\d+/gi, type: 'lishi' },
335
+ { pattern: /https?:\/\/video\.li\/[0-9a-zA-Z_-]{3,}/gi, type: 'lishi' },
336
+ { pattern: /https?:\/\/(?:www\.)?quanmin\.tv\/\w+/gi, type: 'quanmin' },
337
+ { pattern: /https?:\/\/(?:www\.)?quanmintv\.cn\/\w+/gi, type: 'quanmin' },
338
+ { pattern: /https?:\/\/h5\.pipigx\.com\/pp\/post\/\d+/gi, type: 'pipigx' },
339
+ { pattern: /https?:\/\/(?:www\.)?ippzone\.com\/\w+/gi, type: 'pipigx' },
340
+ { pattern: /https?:\/\/(?:h5|www)\.pipix\.com\/\w+/gi, type: 'pipixia' },
341
+ { pattern: /https?:\/\/(?:www\.)?pipixia\.com\/\w+/gi, type: 'pipixia' },
342
+ { pattern: /https?:\/\/share\.xiaochuankeji\.cn\/hybrid\/share\/post\?pid=\d+/gi, type: 'zuiyou' },
343
+ { pattern: /https?:\/\/(?:h5|www)\.izuiyou\.com\/\w+/gi, type: 'zuiyou' },
316
344
  ];
317
345
  function buildCustomLinkRules(customPlatforms) {
318
346
  if (!Array.isArray(customPlatforms) || customPlatforms.length === 0)
@@ -604,6 +632,10 @@ function generateFormattedText(p, format) {
604
632
  '音乐作者': p.music.author || '',
605
633
  '音乐封面': p.music.cover || '',
606
634
  };
635
+ const varReplacements = Object.entries(vars).map(([key, val]) => ({
636
+ regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
637
+ value: val,
638
+ }));
607
639
  const lines = format.split('\n');
608
640
  const resultLines = [];
609
641
  for (const line of lines) {
@@ -622,8 +654,8 @@ function generateFormattedText(p, format) {
622
654
  continue;
623
655
  }
624
656
  let newLine = line;
625
- for (const [key, val] of Object.entries(vars)) {
626
- newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
657
+ for (const { regex, value } of varReplacements) {
658
+ newLine = newLine.replace(regex, value);
627
659
  }
628
660
  resultLines.push(newLine);
629
661
  }
@@ -661,6 +693,8 @@ function parseFieldMapping(mappingStr) {
661
693
  }
662
694
  }
663
695
  function apply(ctx, config) {
696
+ // @ts-expect-error koishi runtime supports optional service dependencies
697
+ ctx.using(['downloads', 'silk', 'ffmpeg'], { optional: true });
664
698
  debugEnabled = config.debug || false;
665
699
  debugLog('INFO', 'plugin start');
666
700
  const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
@@ -674,6 +708,7 @@ function apply(ctx, config) {
674
708
  parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
675
709
  };
676
710
  const proxyConfig = config.proxy || {};
711
+ const cacheDir = config.cacheDir || './temp_cache';
677
712
  const customPlatforms = (config.customPlatforms || []).map((p) => ({
678
713
  name: p.name,
679
714
  apiUrl: p.apiUrl,
@@ -684,6 +719,59 @@ function apply(ctx, config) {
684
719
  proxy: p.proxy || null
685
720
  }));
686
721
  const downloadLimiter = new ConcurrencyLimiter(config.downloadConcurrency || 3);
722
+ const mediaDownloadTimeout = config.mediaDownloadTimeout ?? 120000;
723
+ const maxMediaSize = config.maxMediaSize ?? 0;
724
+ const downloadEngine = config.downloadEngine || 'internal';
725
+ let aria2 = null;
726
+ if (downloadEngine === 'aria2') {
727
+ try {
728
+ const Aria2 = require('aria2');
729
+ aria2 = new Aria2({
730
+ host: config.aria2Host || '127.0.0.1',
731
+ port: config.aria2Port || 6800,
732
+ secure: false,
733
+ secret: config.aria2Secret || '',
734
+ path: '/jsonrpc'
735
+ });
736
+ aria2.open();
737
+ logger.info('aria2 连接成功');
738
+ }
739
+ catch (e) {
740
+ logger.warn('aria2 连接失败,回退到内置下载');
741
+ }
742
+ }
743
+ const apiKeyList = (config.apiKeys || []).map((k) => ({
744
+ key: k.key,
745
+ weight: k.weight || 1,
746
+ lastUsed: 0
747
+ }));
748
+ let keyIndex = 0;
749
+ function getNextApiKey() {
750
+ if (apiKeyList.length === 0)
751
+ return '';
752
+ if (config.rotationMode === 'load_balance') {
753
+ const totalWeight = apiKeyList.reduce((sum, k) => sum + k.weight, 0);
754
+ let rand = Math.random() * totalWeight;
755
+ for (const k of apiKeyList) {
756
+ rand -= k.weight;
757
+ if (rand <= 0)
758
+ return k.key;
759
+ }
760
+ return apiKeyList[0].key;
761
+ }
762
+ else {
763
+ const current = apiKeyList[keyIndex % apiKeyList.length];
764
+ keyIndex++;
765
+ return current.key;
766
+ }
767
+ }
768
+ function markApiKeyInvalid(key) {
769
+ if (config.rotationMode === 'sequential') {
770
+ const idx = apiKeyList.findIndex(k => k.key === key);
771
+ if (idx !== -1)
772
+ apiKeyList.splice(idx, 1);
773
+ }
774
+ }
687
775
  function getPlatformConfig(type) {
688
776
  if (type.startsWith('custom_')) {
689
777
  const name = type.slice(7);
@@ -692,7 +780,7 @@ function apply(ctx, config) {
692
780
  return {
693
781
  apiUrl: custom.apiUrl,
694
782
  dedicatedFirst: true,
695
- apiKey: custom.apiKey,
783
+ apiKey: custom.apiKey || getNextApiKey(),
696
784
  authHeaderType: custom.authHeaderType,
697
785
  customHeaderName: custom.customHeaderName,
698
786
  fieldMapping: custom.fieldMapping,
@@ -706,7 +794,6 @@ function apply(ctx, config) {
706
794
  bilibili: 'https://api.bugpk.com/api/bilibili',
707
795
  douyin: 'https://api.bugpk.com/api/douyin',
708
796
  doubao: 'https://api.bugpk.com/api/dbvideos',
709
- doubao_chat: 'https://api.bugpk.com/api/dbduihua',
710
797
  kuaishou: 'https://api.bugpk.com/api/kuaishou',
711
798
  xiaohongshu: 'https://api.bugpk.com/api/xhs',
712
799
  jimeng: 'https://api.bugpk.com/api/jimengai',
@@ -725,11 +812,14 @@ function apply(ctx, config) {
725
812
  let fieldMapping = undefined;
726
813
  if (custom && custom.apiUrl) {
727
814
  apiUrl = custom.apiUrl;
728
- apiKey = custom.apiKey || '';
815
+ apiKey = custom.apiKey || getNextApiKey();
729
816
  authHeaderType = custom.authHeaderType || 'Bearer';
730
817
  customHeaderName = custom.customHeaderName || 'X-API-Key';
731
818
  fieldMapping = parseFieldMapping(custom.fieldMapping);
732
819
  }
820
+ else {
821
+ apiKey = getNextApiKey();
822
+ }
733
823
  const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
734
824
  if (!fieldMapping) {
735
825
  fieldMapping = parseFieldMapping(config.globalFieldMapping);
@@ -762,13 +852,77 @@ function apply(ctx, config) {
762
852
  return cleanUrl(url);
763
853
  }
764
854
  }
765
- async function downloadFile(url, timeout, maxSize, tempDir, filePrefix, fileExts) {
855
+ async function downloadFile(url, timeout, maxSize, filePrefix, fileExts) {
766
856
  if (!url)
767
857
  throw new Error('链接为空');
768
- await promises_1.default.mkdir(tempDir, { recursive: true });
769
- const ext = fileExts.find(e => url.match(new RegExp('\\.' + e + '(\\?|$)', 'i'))) || fileExts[0];
858
+ await promises_1.default.mkdir(cacheDir, { recursive: true });
859
+ const extRegexCache = {};
860
+ const ext = fileExts.find(e => {
861
+ const r = extRegexCache[e] || (extRegexCache[e] = new RegExp('\\.' + e + '(\\?|$)', 'i'));
862
+ return r.test(url);
863
+ }) || fileExts[0];
770
864
  const fileName = `${filePrefix}_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
771
- const filePath = path_1.default.resolve(tempDir, fileName);
865
+ const filePath = path_1.default.resolve(cacheDir, fileName);
866
+ if (ctx.downloads) {
867
+ try {
868
+ const dest = await ctx.downloads.download(url, path_1.default.join(cacheDir, fileName), {
869
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
870
+ timeout
871
+ });
872
+ const stat = await promises_1.default.stat(dest);
873
+ if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
874
+ await promises_1.default.unlink(dest).catch(() => { });
875
+ throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
876
+ }
877
+ return dest;
878
+ }
879
+ catch (e) {
880
+ debugLog('ERROR', `downloads 服务下载失败,回退: ${getErrorMessage(e)}`);
881
+ }
882
+ }
883
+ if (aria2 && config.resumeDownload) {
884
+ try {
885
+ const gid = await aria2.call('aria2.addUri', [url], {
886
+ dir: cacheDir,
887
+ out: fileName,
888
+ split: 4,
889
+ continue: true,
890
+ maxConnectionPerServer: 5,
891
+ timeout: timeout / 1000,
892
+ maxFileNotFound: 5,
893
+ maxTries: 5,
894
+ retryWait: 2,
895
+ header: [`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36`, `Referer: https://www.baidu.com/`]
896
+ });
897
+ let completed = false;
898
+ const ariaStartTime = Date.now();
899
+ while (!completed) {
900
+ if (Date.now() - ariaStartTime > timeout) {
901
+ await aria2.call('aria2.remove', gid).catch(() => { });
902
+ throw new Error('aria2下载超时');
903
+ }
904
+ const status = await aria2.call('aria2.tellStatus', gid);
905
+ if (status.status === 'complete') {
906
+ completed = true;
907
+ }
908
+ else if (status.status === 'error' || status.status === 'removed') {
909
+ throw new Error('aria2下载失败');
910
+ }
911
+ else {
912
+ await delay(1000);
913
+ }
914
+ }
915
+ const stat = await promises_1.default.stat(filePath);
916
+ if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
917
+ await promises_1.default.unlink(filePath).catch(() => { });
918
+ throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
919
+ }
920
+ return filePath;
921
+ }
922
+ catch (e) {
923
+ debugLog('ERROR', `aria2下载失败,回退内置下载: ${getErrorMessage(e)}`);
924
+ }
925
+ }
772
926
  const writer = (0, fs_1.createWriteStream)(filePath);
773
927
  let response;
774
928
  try {
@@ -803,7 +957,7 @@ function apply(ctx, config) {
803
957
  throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
804
958
  }
805
959
  }
806
- async function sendMedia(session, url, type, forceDownload, showFile, timeout, tempDir, maxSize) {
960
+ async function sendMedia(session, url, type, forceDownload, showFile) {
807
961
  if (!url)
808
962
  return;
809
963
  await downloadLimiter.acquire();
@@ -818,7 +972,7 @@ function apply(ctx, config) {
818
972
  const sendFunc = type === 'audio' ? koishi_1.h.audio : type === 'video' ? koishi_1.h.video : koishi_1.h.image;
819
973
  if (forceDownload) {
820
974
  try {
821
- const localPath = await downloadFile(url, timeout, maxSize, tempDir, prefixMap[type], extMap[type]);
975
+ const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type]);
822
976
  try {
823
977
  await sendWithTimeout(session, sendFunc(`file://${localPath}`));
824
978
  }
@@ -847,7 +1001,7 @@ function apply(ctx, config) {
847
1001
  }
848
1002
  catch {
849
1003
  try {
850
- const localPath = await downloadFile(url, timeout, maxSize, tempDir, prefixMap[type], extMap[type]);
1004
+ const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type]);
851
1005
  try {
852
1006
  await sendWithTimeout(session, sendFunc(`file://${localPath}`));
853
1007
  }
@@ -957,26 +1111,26 @@ function apply(ctx, config) {
957
1111
  await delay(300);
958
1112
  }
959
1113
  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(() => { });
1114
+ await sendMedia(session, p.cover, 'image', config.forceDownloadImage, config.showImageFile).catch(() => { });
961
1115
  await delay(300);
962
1116
  }
963
1117
  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(() => { });
1118
+ await sendMedia(session, p.music.cover, 'image', config.forceDownloadImage, config.showImageFile).catch(() => { });
965
1119
  await delay(300);
966
1120
  }
967
1121
  if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
968
- await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile, config.videoDownloadTimeout, config.tempDir, config.maxVideoSize).catch(() => { });
1122
+ await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile).catch(() => { });
969
1123
  await delay(500);
970
1124
  }
971
1125
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
972
1126
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
973
1127
  for (const imgUrl of imageUrls) {
974
- await sendMedia(session, imgUrl, 'image', config.forceDownloadImage, config.showImageFile, config.imageDownloadTimeout, config.imageTempDir, config.maxImageSize).catch(() => { });
1128
+ await sendMedia(session, imgUrl, 'image', config.forceDownloadImage, config.showImageFile).catch(() => { });
975
1129
  await delay(200);
976
1130
  }
977
1131
  }
978
1132
  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(() => { });
1133
+ await sendMedia(session, p.music.url, 'audio', config.forceDownloadMusicVoice, config.showMusicVoiceFile).catch(() => { });
980
1134
  await delay(300);
981
1135
  }
982
1136
  }
@@ -1045,6 +1199,10 @@ function apply(ctx, config) {
1045
1199
  urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
1046
1200
  return parsed;
1047
1201
  }
1202
+ if (res.data?.code === 403 || res.data?.code === 401) {
1203
+ if (api.apiKey)
1204
+ markApiKeyInvalid(api.apiKey);
1205
+ }
1048
1206
  throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
1049
1207
  }
1050
1208
  catch (error) {
@@ -1170,19 +1328,16 @@ function apply(ctx, config) {
1170
1328
  });
1171
1329
  const tempCleanupInterval = setInterval(async () => {
1172
1330
  try {
1173
- const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images', config.musicTempDir || './temp_music'];
1174
- for (const dir of dirs) {
1175
- const files = await promises_1.default.readdir(dir);
1176
- const now = Date.now();
1177
- for (const file of files) {
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))) {
1181
- const filePath = path_1.default.join(dir, file);
1182
- const stats = await promises_1.default.stat(filePath);
1183
- if (now - stats.mtimeMs > 3600000) {
1184
- await promises_1.default.unlink(filePath).catch(() => { });
1185
- }
1331
+ const files = await promises_1.default.readdir(cacheDir);
1332
+ const now = Date.now();
1333
+ for (const file of files) {
1334
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1335
+ (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1336
+ (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1337
+ const filePath = path_1.default.join(cacheDir, file);
1338
+ const stats = await promises_1.default.stat(filePath);
1339
+ if (now - stats.mtimeMs > 3600000) {
1340
+ await promises_1.default.unlink(filePath).catch(() => { });
1186
1341
  }
1187
1342
  }
1188
1343
  }
@@ -1193,21 +1348,20 @@ function apply(ctx, config) {
1193
1348
  }, 3600000);
1194
1349
  ctx.on('dispose', () => {
1195
1350
  clearInterval(tempCleanupInterval);
1351
+ if (aria2)
1352
+ aria2.close();
1196
1353
  urlCacheLocal.clear();
1197
1354
  dedupCache.clear();
1198
1355
  debugLog('INFO', '插件已卸载');
1199
1356
  });
1200
1357
  process.on('beforeExit', async () => {
1201
1358
  try {
1202
- const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images', config.musicTempDir || './temp_music'];
1203
- for (const dir of dirs) {
1204
- const files = await promises_1.default.readdir(dir);
1205
- for (const file of files) {
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))) {
1209
- await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
1210
- }
1359
+ const files = await promises_1.default.readdir(cacheDir);
1360
+ for (const file of files) {
1361
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1362
+ (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1363
+ (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1364
+ await promises_1.default.unlink(path_1.default.join(cacheDir, file)).catch(() => { });
1211
1365
  }
1212
1366
  }
1213
1367
  }
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.5",
4
+ "version": "1.3.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -45,7 +45,6 @@
45
45
  "twitter",
46
46
  "instagram",
47
47
  "doubao",
48
- "doubao_chat",
49
48
  "jimeng",
50
49
  "oasis",
51
50
  "wechat_channel",
@@ -94,14 +93,14 @@
94
93
  "typescript": "^5.3.3"
95
94
  },
96
95
  "dependencies": {
97
- "axios": "^1.16.1",
98
- "fast-xml-parser": "^4.5.6"
96
+ "axios": "^1.16.1"
99
97
  },
100
98
  "peerDependencies": {
101
99
  "@koishijs/plugin-console": "^5.30.4",
102
100
  "koishi": "^4.18.7",
103
101
  "koishi-plugin-silk": "^1.0.0",
104
- "koishi-plugin-ffmpeg": "^1.0.0"
102
+ "koishi-plugin-ffmpeg": "^1.0.0",
103
+ "aria2": "^4.1.2"
105
104
  },
106
105
  "peerDependenciesMeta": {
107
106
  "koishi-plugin-silk": {
@@ -109,6 +108,9 @@
109
108
  },
110
109
  "koishi-plugin-ffmpeg": {
111
110
  "optional": true
111
+ },
112
+ "aria2": {
113
+ "optional": true
112
114
  }
113
115
  },
114
116
  "repository": {
package/readme.md CHANGED
@@ -20,89 +20,94 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
20
20
 
21
21
  ## 配置项说明 (Configuration)
22
22
 
23
- ### 基础设置 (Basic Settings)
23
+ ### 基本设置
24
24
  | 配置项 | 类型 | 默认值 | 说明 |
25
25
  |--------|------|--------|------|
26
- | `enable` | boolean | true | 是否启用视频解析插件 |
27
- | `botName` | string | 视频解析机器人 | 合并转发消息中显示的机器人名称 |
28
- | `showWaitingTip` | boolean | true | 解析时是否显示等待提示 |
29
- | `debug` | boolean | false | 是否开启 Debug 模式,在控制台输出详细日志 |
30
- | `platformEnabled` | object | 各平台均为 `true` | 各平台解析开关,可单独关闭某平台 |
26
+ | `enable` | boolean | true | 启用插件 |
27
+ | `botName` | string | 视频解析机器人 | 合并转发中的昵称 |
28
+ | `showWaitingTip` | boolean | true | 显示等待提示 |
29
+ | `debug` | boolean | false | Debug 日志 |
30
+ | `platformEnabled` | object | 全开 | 各平台开关 |
31
31
 
32
- ### 统一消息格式 (Unified Message Format)
32
+ ### 消息格式
33
33
  | 配置项 | 类型 | 默认值 | 说明 |
34
34
  |--------|------|--------|------|
35
- | `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 文字消息格式,支持变量替换。空行自动隐藏。 |
35
+ | `unifiedMessageFormat` | string | 见预设 | 文字格式,支持变量,空行自动隐藏 |
36
36
 
37
- ### 内容显示设置 (Content Display Settings)
37
+ ### 媒体发送
38
38
  | 配置项 | 类型 | 默认值 | 说明 |
39
39
  |--------|------|--------|------|
40
- | `showImageText` | boolean | true | 是否发送文字内容 |
41
- | `showCoverImage` | boolean | true | 是否发送封面图片(视频/图集封面) |
42
- | `showMusicCover` | boolean | true | 是否发送音乐封面图片 |
43
- | `showImageFile` | boolean | true | 封面/图片是否以文件形式发送(关闭则只发链接) |
44
- | `forceDownloadImage` | boolean | false | 强制下载封面/图片后发送 |
45
- | `imageDownloadTimeout` | number | 60000 | 图片下载超时(毫秒) |
46
- | `imageTempDir` | string | `./temp_images` | 临时封面/图片存储目录 |
47
- | `maxImageSize` | number | 0 | 最大下载图片大小(MB),0 不限制 |
48
- | `showVideoFile` | boolean | true | 视频是否以文件形式发送(关闭则只发链接) |
49
- | `forceDownloadVideo` | boolean | false | 强制下载视频后发送 |
50
- | `videoDownloadTimeout` | number | 120000 | 视频下载超时(毫秒) |
51
- | `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
52
- | `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 不限制 |
53
- | `maxDescLength` | number | 200 | 简介最大长度(字符) |
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 不限制 |
62
-
63
- ### 网络与 API 设置 (Network & API Settings)
40
+ | `showImageText` | boolean | true | 发送文字内容 |
41
+ | `showCoverImage` | boolean | true | 发送封面图片 |
42
+ | `showMusicCover` | boolean | true | 发送音乐封面 |
43
+ | `showImageFile` | boolean | true | 封面/图片是否以图片形式发送(关闭则只发送链接) |
44
+ | `showVideoFile` | boolean | true | 视频是否以视频形式发送(关闭则只发送链接) |
45
+ | `forceDownloadImage` | boolean | false | 强制下载封面/图片 |
46
+ | `forceDownloadVideo` | boolean | false | 强制下载视频 |
47
+
48
+ ### 音乐语音(需 silk ffmpeg)
64
49
  | 配置项 | 类型 | 默认值 | 说明 |
65
50
  |--------|------|--------|------|
66
- | `timeout` | number | 180000 | API 请求超时时间(毫秒) |
67
- | `videoSendTimeout` | number | 60000 | 消息发送超时时间(毫秒,0 为不限制) |
68
- | `userAgent` | string | `Mozilla/5.0 ...` | User-Agent |
69
- | `proxy` | object | `{ enabled: false, protocol: "http", host: "127.0.0.1", port: 7890, auth: { username: "", password: "" } }` | HTTP/HTTPS 代理。`enabled` 开关(默认关闭),`protocol` 下拉选择 `http` 或 `https` |
70
- | `customHeaders` | array | [] | 自定义请求头,每项含 `name` 和 `value` |
51
+ | `showMusicVoice` | boolean | false | 音乐链接以语音发送 |
52
+ | `showMusicVoiceFile` | boolean | true | 音乐语音是否以文件形式发送(关闭则只发送链接) |
53
+ | `forceDownloadMusicVoice` | boolean | false | 强制下载音乐语音 |
71
54
 
72
- ### API 选择与回退设置 (API Selection & Fallback)
55
+ ### 性能与限制
73
56
  | 配置项 | 类型 | 默认值 | 说明 |
74
57
  |--------|------|--------|------|
75
- | `platformDedicatedFirst` | object | 各平台均为 `false` | 平台专属 API 优先开关,键:`bilibili` 等 |
76
- | `customApis` | array | [] | 自定义内置平台专属 API,含 `platform`, `apiUrl`, `apiKey`, `authHeaderType`, `customHeaderName`, `fieldMapping` |
77
- | `customPlatforms` | array | [] | 完全自定义新平台。每项含:`name`(平台名称)、`exampleUrl`(示例链接)、`keywords`(匹配关键词,逗号分隔)、`apiUrl`(解析API)、`apiKey`、`authHeaderType`、`customHeaderName`、`fieldMapping`、`proxy`(独立代理) |
78
- | `globalFieldMapping` | string | 预设字段映射 JSON | 全局字段映射,支持点号路径 |
58
+ | `maxDescLength` | number | 200 | 简介长度上限 |
59
+ | `maxConcurrent` | number | 3 | 解析最大并发数 |
60
+ | `downloadConcurrency` | number | 3 | 下载线程数 |
61
+ | `mediaDownloadTimeout` | number | 120000 | 统一下载超时 (ms) |
62
+ | `maxMediaSize` | number | 0 | 最大下载文件大小 (MB),0 为不限制 |
63
+ | `downloadEngine` | string | internal | 下载引擎(internal / aria2) |
64
+ | `aria2Host` | string | 127.0.0.1 | aria2 RPC 地址 |
65
+ | `aria2Port` | number | 6800 | aria2 RPC 端口 |
66
+ | `aria2Secret` | string | | aria2 RPC 密钥 |
67
+ | `resumeDownload` | boolean | true | 启用断点续传(仅 aria2) |
68
+
69
+ ### 网络与请求
70
+ | 配置项 | 类型 | 默认值 | 说明 |
71
+ |--------|------|--------|------|
72
+ | `timeout` | number | 180000 | API 超时 (ms) |
73
+ | `videoSendTimeout` | number | 180000 | 发送超时 (ms) |
74
+ | `userAgent` | string | 见预设 | User-Agent |
75
+ | `proxy` | object | ... | HTTP/HTTPS 代理 |
76
+ | `customHeaders` | array | [] | 自定义请求头 |
79
77
 
80
- ### 错误与重试设置 (Error & Retry Settings)
78
+ ### 发送与重试
81
79
  | 配置项 | 类型 | 默认值 | 说明 |
82
80
  |--------|------|--------|------|
83
81
  | `ignoreSendError` | boolean | true | 忽略发送失败 |
84
82
  | `retryTimes` | number | 3 | 重试次数 |
85
- | `retryInterval` | number | 1000 | 重试间隔(毫秒) |
83
+ | `retryInterval` | number | 1000 | 重试间隔 (ms) |
84
+ | `enableForward` | boolean | false | 合并转发(OneBot/Satori) |
86
85
 
87
- ### 发送方式设置 (Send Mode Settings)
86
+ ### 缓存与临时文件
88
87
  | 配置项 | 类型 | 默认值 | 说明 |
89
88
  |--------|------|--------|------|
90
- | `enableForward` | boolean | false | 启用合并转发(支持 OneBot、Satori 平台) |
89
+ | `deduplicationInterval` | number | 180 | 去重间隔 (s) |
90
+ | `cacheTTL` | number | 600 | 缓存时间 (s) |
91
+ | `cacheDir` | string | ./temp_cache | 统一临时目录 |
91
92
 
92
- ### 缓存与去重设置 (Cache & Deduplication Settings)
93
+ ### API 与平台
93
94
  | 配置项 | 类型 | 默认值 | 说明 |
94
95
  |--------|------|--------|------|
95
- | `deduplicationInterval` | number | 180 | 去重间隔(秒) |
96
- | `cacheTTL` | number | 600 | 缓存时间(秒) |
97
-
98
- ### 界面文字设置 (UI Text Settings)
96
+ | `apiKeys` | array | [] | API 密钥列表 |
97
+ | `rotationMode` | string | sequential | 密钥轮换模式(sequential / load_balance) |
98
+ | `platformDedicatedFirst` | object | 全关 | 优先专属 API |
99
+ | `customApis` | array | [] | 覆盖内置平台 API |
100
+ | `customPlatforms` | array | [] | 自定义新平台 |
101
+ | `globalFieldMapping` | string | 预设 | 全局字段映射 JSON |
102
+
103
+ ### 界面文本
99
104
  | 配置项 | 类型 | 默认值 | 说明 |
100
105
  |--------|------|--------|------|
101
- | `waitingTipText` | string | 正在解析视频,请稍候... | 等待提示 |
102
- | `unsupportedPlatformText` | string | 不支持该平台链接 | 不支持平台提示 |
103
- | `invalidLinkText` | string | 无效的视频链接 | 无效链接提示 |
106
+ | `waitingTipText` | string | 正在解析... | 等待提示 |
107
+ | `unsupportedPlatformText` | string | 不支持该平台 | 不支持提示 |
108
+ | `invalidLinkText` | string | 无效链接 | 无效链接提示 |
104
109
  | `parseErrorPrefix` | string | ❌ 解析失败: | 错误前缀 |
105
- | `parseErrorItemFormat` | string | `【${url}】: ${msg}` | 错误项格式 |
110
+ | `parseErrorItemFormat` | string | ... | 错误格式 |
106
111
 
107
112
  ## 支持的变量 (Supported Variables)
108
113
  在 `unifiedMessageFormat` 中可使用以下变量,空行自动隐藏:
@@ -125,13 +130,19 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
125
130
  | `${音乐标题}` | 音乐标题 |
126
131
  | `${音乐作者}` | 音乐作者 |
127
132
 
128
- ## 音乐语音依赖说明 (Music Voice Dependencies)
129
- 若启用 `showMusicVoice`,请确保已安装以下 Koishi 插件:
130
- - `koishi-plugin-silk`:提供 silk 编解码支持
131
- - `koishi-plugin-ffmpeg`:提供音频重采样支持
132
-
133
- 这些依赖已声明为可选依赖
134
- 若未安装,音乐语音将尝试直接发送原始音频,部分平台可能无法播放。
133
+ ## 依赖说明 (Dependencies)
134
+ ### 音乐语音(可选)
135
+ 若启用 `showMusicVoice`,请安装:
136
+ - `koishi-plugin-silk`:silk 编解码
137
+ - `koishi-plugin-ffmpeg`:音频重采样
138
+ ### aria2 下载引擎(可选)
139
+ 若启用 `downloadEngine: 'aria2'`,请安装并启动 aria2 服务,并安装 npm 包 `aria2`:
140
+ - 安装 aria2 服务端:https://github.com/aria2/aria2
141
+ - 安装 npm 客户端:`npm install aria2`
142
+ - 启动 RPC:`aria2c --enable-rpc --rpc-listen-all=true --rpc-allow-origin-all`
143
+ 未满足条件时自动降级为内置下载,不影响正常使用。
144
+ ### downloads 服务(可选)
145
+ 插件会自动检测 Koishi 内置 `downloads` 服务,优先使用其下载文件,失败时回退到内置/aria2 下载。
135
146
 
136
147
  ## 支持的平台 (Supported Platforms)
137
148
  | 平台名称 | 关键词识别 | 解析能力 |
@@ -150,22 +161,19 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
150
161
  | YouTube(油管) | youtube, youtu.be | 视频 |
151
162
  | TikTok(国际版抖音) | tiktok, tiktok.com | 短视频 |
152
163
  | 好看视频 | haokan, haokan.baidu.com | 短视频 |
153
- | 梨视频 | video.li | 短视频 |
154
164
  | 美拍 | meipai, meipai.com | 短视频 |
155
- | 全民直播 | quanmin (quanmin.tv) | 直播 |
156
165
  | Twitter / X | twitter, x.com | 视频、图文 |
157
166
  | Instagram | instagram, instagram.com | 图文、Reels |
158
167
  | 豆包 | doubao (doubao.com/video) | 视频 |
159
- | 豆包对话 | doubao (doubao.com/thread) | 对话分享 |
160
168
  | 皮皮搞笑 | pipigx, h5.pipigx.com | 短视频 |
161
169
  | 皮皮虾 | pipixia, h5.pipix.com | 短视频 |
162
170
  | 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
171
+ | 梨视频 | video.li, pearvideo.com | 短视频 |
172
+ | 全民直播 | quanmin (quanmin.tv) | 直播 |
163
173
  | 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
164
174
  | 视频号 (WeChat Channels) | channels.weixin.qq.com, weixin.qq.com/sph/ | 短视频 |
165
175
  | 🔧 自定义平台 | 通过 `customPlatforms` 添加 | 取决于 API |
166
176
 
167
- > 注:部分平台解析能力可能因API限制有所差异。可通过 `platformEnabled` 单独关闭。
168
-
169
177
  ## 项目贡献者 (Contributors)
170
178
 
171
179
  | 贡献者 (Contributor) | 贡献内容 (Contribution) |