koishi-plugin-video-parser-all 1.3.2 → 1.3.4

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
@@ -10,12 +10,18 @@ export declare const Config: Schema<{
10
10
  } & {
11
11
  showImageText?: boolean | null | undefined;
12
12
  showCoverImage?: boolean | null | undefined;
13
+ showMusicCover?: boolean | null | undefined;
14
+ showImageFile?: boolean | null | undefined;
15
+ forceDownloadImage?: boolean | null | undefined;
16
+ imageDownloadTimeout?: number | null | undefined;
17
+ imageTempDir?: string | null | undefined;
18
+ maxImageSize?: number | null | undefined;
13
19
  showVideoFile?: boolean | null | undefined;
14
- maxDescLength?: number | null | undefined;
20
+ forceDownloadVideo?: boolean | null | undefined;
15
21
  videoDownloadTimeout?: number | null | undefined;
16
22
  tempDir?: string | null | undefined;
17
23
  maxVideoSize?: number | null | undefined;
18
- forceDownloadVideo?: boolean | null | undefined;
24
+ maxDescLength?: number | null | undefined;
19
25
  maxConcurrent?: number | null | undefined;
20
26
  } & {
21
27
  timeout?: number | null | undefined;
@@ -94,12 +100,18 @@ export declare const Config: Schema<{
94
100
  } & {
95
101
  showImageText: boolean;
96
102
  showCoverImage: boolean;
103
+ showMusicCover: boolean;
104
+ showImageFile: boolean;
105
+ forceDownloadImage: boolean;
106
+ imageDownloadTimeout: number;
107
+ imageTempDir: string;
108
+ maxImageSize: number;
97
109
  showVideoFile: boolean;
98
- maxDescLength: number;
110
+ forceDownloadVideo: boolean;
99
111
  videoDownloadTimeout: number;
100
112
  tempDir: string;
101
113
  maxVideoSize: number;
102
- forceDownloadVideo: boolean;
114
+ maxDescLength: number;
103
115
  maxConcurrent: number;
104
116
  } & {
105
117
  timeout: number;
package/lib/index.js CHANGED
@@ -76,78 +76,84 @@ exports.Config = koishi_1.Schema.intersect([
76
76
  debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
77
77
  }).description('基础设置'),
78
78
  koishi_1.Schema.object({
79
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n音乐封面:${音乐封面}\n音乐链接:${音乐链接}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID} ${音乐标题} ${音乐作者} ${音乐封面} ${音乐链接}'),
79
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n音乐链接:${音乐链接}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}').description('文字消息格式,支持变量。某行所有变量为空时自动隐藏。封面及媒体文件由独立开关控制,默认不包含在文字中'),
80
80
  }).description('消息格式设置'),
81
81
  koishi_1.Schema.object({
82
- showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
83
- showCoverImage: koishi_1.Schema.boolean().default(true).description('是否发送封面图片'),
84
- showVideoFile: koishi_1.Schema.boolean().default(true).description('是否发送视频文件(关闭则只发送视频链接)'),
85
- maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介内容最大长度(字符),超出自动截断'),
82
+ showImageText: koishi_1.Schema.boolean().default(true).description('是否发送文字内容'),
83
+ showCoverImage: koishi_1.Schema.boolean().default(true).description('是否发送封面图片(视频/图集封面)'),
84
+ showMusicCover: koishi_1.Schema.boolean().default(true).description('是否发送音乐封面图片'),
85
+ showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以文件形式发送(关闭则只发送链接)'),
86
+ forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片后发送'),
87
+ imageDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('图片下载超时(毫秒)'),
88
+ imageTempDir: koishi_1.Schema.string().default('./temp_images').description('临时封面/图片存储目录'),
89
+ maxImageSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载图片大小(MB),0 为不限制'),
90
+ showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以文件形式发送(关闭则只发送链接)'),
91
+ forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频后发送'),
86
92
  videoDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('视频下载超时(毫秒)'),
87
93
  tempDir: koishi_1.Schema.string().default('./temp_videos').description('临时视频存储目录'),
88
- maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制大小'),
89
- forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频后发送'),
90
- maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('批量解析时最大并发数'),
94
+ maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
95
+ maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介最大长度(字符)'),
96
+ maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('批量解析最大并发数'),
91
97
  }).description('内容显示设置'),
92
98
  koishi_1.Schema.object({
93
99
  timeout: koishi_1.Schema.number().min(0).step(1).default(180000).description('API 请求超时(毫秒)'),
94
- videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('视频消息发送超时(毫秒,0 为不限制)'),
95
- 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('API 请求 UA'),
100
+ videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('消息发送超时(毫秒)'),
101
+ 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'),
96
102
  proxy: koishi_1.Schema.object({
97
- enabled: koishi_1.Schema.boolean().default(false).description('是否启用 HTTP/HTTPS 代理'),
103
+ enabled: koishi_1.Schema.boolean().default(false).description('启用代理'),
98
104
  protocol: koishi_1.Schema.union([
99
105
  koishi_1.Schema.const('http').description('HTTP'),
100
106
  koishi_1.Schema.const('https').description('HTTPS'),
101
- ]).default('http').description('代理协议'),
102
- host: koishi_1.Schema.string().default('127.0.0.1').description('代理地址'),
103
- port: koishi_1.Schema.number().default(7890).description('代理端口'),
107
+ ]).default('http').description('协议'),
108
+ host: koishi_1.Schema.string().default('127.0.0.1').description('地址'),
109
+ port: koishi_1.Schema.number().default(7890).description('端口'),
104
110
  auth: koishi_1.Schema.object({
105
- username: koishi_1.Schema.string().default('').description('代理用户名'),
106
- password: koishi_1.Schema.string().default('').description('代理密码'),
107
- }).description('代理认证'),
108
- }).description('HTTP/HTTPS 代理设置(需开启 enabled)'),
111
+ username: koishi_1.Schema.string().default('').description('用户名'),
112
+ password: koishi_1.Schema.string().default('').description('密码'),
113
+ }).description('认证'),
114
+ }).description('HTTP/HTTPS 代理'),
109
115
  customHeaders: koishi_1.Schema.array(koishi_1.Schema.object({
110
- name: koishi_1.Schema.string().required().description('请求头名称'),
111
- value: koishi_1.Schema.string().required().description('请求头值'),
112
- })).default([]).description('自定义请求头,会附加到所有 API 请求中'),
113
- }).description('网络与 API 设置'),
116
+ name: koishi_1.Schema.string().required().description('头名称'),
117
+ value: koishi_1.Schema.string().required().description('头值'),
118
+ })).default([]).description('自定义请求头'),
119
+ }).description('网络设置'),
114
120
  koishi_1.Schema.object({
115
- ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送失败,避免插件崩溃'),
116
- retryTimes: koishi_1.Schema.number().min(0).step(1).default(3).description('API 请求及消息发送失败时的重试次数'),
117
- retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒,同时用于消息发送重试)'),
118
- }).description('错误与重试设置'),
121
+ ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败'),
122
+ retryTimes: koishi_1.Schema.number().min(0).step(1).default(3).description('重试次数'),
123
+ retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
124
+ }).description('错误与重试'),
119
125
  koishi_1.Schema.object({
120
- enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
121
- }).description('发送方式设置'),
126
+ enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot'),
127
+ }).description('发送方式'),
122
128
  koishi_1.Schema.object({
123
- deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('禁止重复解析时间间隔(秒),0 为不限制'),
124
- cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('解析结果缓存时间(秒),0 为不缓存'),
125
- }).description('缓存与去重设置'),
129
+ deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
130
+ cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
131
+ }).description('缓存与去重'),
126
132
  koishi_1.Schema.object({
127
- primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
128
- backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址'),
133
+ primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API'),
134
+ backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API'),
129
135
  platformDedicatedFirst: koishi_1.Schema.object({
130
- bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩 - 优先使用专属 API'),
131
- douyin: koishi_1.Schema.boolean().default(false).description('抖音 - 优先使用专属 API'),
132
- kuaishou: koishi_1.Schema.boolean().default(false).description('快手 - 优先使用专属 API'),
133
- xiaohongshu: koishi_1.Schema.boolean().default(false).description('小红书 - 优先使用专属 API'),
134
- weibo: koishi_1.Schema.boolean().default(false).description('微博 - 优先使用专属 API'),
135
- xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频 - 优先使用专属 API'),
136
- youtube: koishi_1.Schema.boolean().default(false).description('YouTube - 优先使用专属 API'),
137
- tiktok: koishi_1.Schema.boolean().default(false).description('TikTok - 优先使用专属 API'),
138
- acfun: koishi_1.Schema.boolean().default(false).description('AcFun - 优先使用专属 API'),
139
- zhihu: koishi_1.Schema.boolean().default(false).description('知乎 - 优先使用专属 API'),
140
- weishi: koishi_1.Schema.boolean().default(false).description('微视 - 优先使用专属 API'),
141
- huya: koishi_1.Schema.boolean().default(false).description('虎牙 - 优先使用专属 API'),
142
- haokan: koishi_1.Schema.boolean().default(false).description('好看视频 - 优先使用专属 API'),
143
- meipai: koishi_1.Schema.boolean().default(false).description('美拍 - 优先使用专属 API'),
144
- twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X - 优先使用专属 API'),
145
- instagram: koishi_1.Schema.boolean().default(false).description('Instagram - 优先使用专属 API'),
146
- doubao: koishi_1.Schema.boolean().default(false).description('豆包 - 优先使用专属 API'),
147
- doubao_chat: koishi_1.Schema.boolean().default(false).description('豆包对话 - 优先使用专属 API'),
148
- oasis: koishi_1.Schema.boolean().default(false).description('绿洲 - 优先使用专属 API'),
149
- wechat_channel: koishi_1.Schema.boolean().default(false).description('视频号 - 优先使用专属 API'),
150
- }).description('各平台独立开关:是否优先使用专属 API'),
136
+ bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
137
+ douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
138
+ kuaishou: koishi_1.Schema.boolean().default(false).description('快手'),
139
+ xiaohongshu: koishi_1.Schema.boolean().default(false).description('小红书'),
140
+ weibo: koishi_1.Schema.boolean().default(false).description('微博'),
141
+ xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
142
+ youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
143
+ tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
144
+ acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
145
+ zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
146
+ weishi: koishi_1.Schema.boolean().default(false).description('微视'),
147
+ huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
148
+ haokan: koishi_1.Schema.boolean().default(false).description('好看视频'),
149
+ meipai: koishi_1.Schema.boolean().default(false).description('美拍'),
150
+ twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X'),
151
+ instagram: koishi_1.Schema.boolean().default(false).description('Instagram'),
152
+ doubao: koishi_1.Schema.boolean().default(false).description('豆包'),
153
+ doubao_chat: koishi_1.Schema.boolean().default(false).description('豆包对话'),
154
+ oasis: koishi_1.Schema.boolean().default(false).description('绿洲'),
155
+ wechat_channel: koishi_1.Schema.boolean().default(false).description('视频号'),
156
+ }).description('优先使用专属 API'),
151
157
  customApis: koishi_1.Schema.array(koishi_1.Schema.object({
152
158
  platform: koishi_1.Schema.union([
153
159
  koishi_1.Schema.const('bilibili').description('哔哩哔哩'),
@@ -170,17 +176,17 @@ exports.Config = koishi_1.Schema.intersect([
170
176
  koishi_1.Schema.const('doubao_chat').description('豆包对话'),
171
177
  koishi_1.Schema.const('oasis').description('绿洲'),
172
178
  koishi_1.Schema.const('wechat_channel').description('视频号'),
173
- ]).description('选择平台'),
179
+ ]).description('平台'),
174
180
  apiUrl: koishi_1.Schema.string().description('API 地址'),
175
- apiKey: koishi_1.Schema.string().description('API Key(可选)').default(''),
181
+ apiKey: koishi_1.Schema.string().description('API Key').default(''),
176
182
  authHeaderType: koishi_1.Schema.union([
177
- koishi_1.Schema.const('Bearer').description('Bearer Token'),
183
+ koishi_1.Schema.const('Bearer').description('Bearer'),
178
184
  koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
179
- koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
185
+ koishi_1.Schema.const('Custom').description('自定义'),
180
186
  ]).default('Bearer').description('认证头类型'),
181
- customHeaderName: koishi_1.Schema.string().description('自定义 Header 名称(仅当选择 Custom 时有效)').default('X-API-Key'),
182
- fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON,例如 {"title":"data.info.name"},支持点号路径'),
183
- })).default([]).description('自定义平台专属 API 地址,留空则使用内置默认专属 API'),
187
+ customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
188
+ fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
189
+ })).default([]).description('自定义专属 API'),
184
190
  globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
185
191
  ' "title": "data.title",\n' +
186
192
  ' "desc": "data.description",\n' +
@@ -203,15 +209,15 @@ exports.Config = koishi_1.Schema.intersect([
203
209
  ' "music_author": "data.music.author",\n' +
204
210
  ' "music_cover": "data.music.cover",\n' +
205
211
  ' "music_url": "data.music.url"\n' +
206
- '}').description('全局字段映射 JSON,优先级低于专属 API 映射'),
207
- }).description('API 选择设置'),
212
+ '}').description('全局字段映射 JSON'),
213
+ }).description('API 选择'),
208
214
  koishi_1.Schema.object({
209
- waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示文字'),
210
- unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示文字'),
211
- invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示(parse 指令)'),
212
- parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('解析失败消息前缀'),
213
- parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败的展示格式,可用 ${url}(链接)和 ${msg}(错误信息)'),
214
- }).description('界面文字设置'),
215
+ waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示'),
216
+ unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持提示'),
217
+ invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示'),
218
+ parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('错误前缀'),
219
+ parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('错误项格式'),
220
+ }).description('界面文字'),
215
221
  ]);
216
222
  const logger = new koishi_1.Logger(exports.name);
217
223
  let debugEnabled = false;
@@ -523,7 +529,6 @@ function generateFormattedText(p, format) {
523
529
  '发布时间': p.publishTime ? formatPublishTime(p.publishTime) : '',
524
530
  '图片数量': String(imageCount),
525
531
  '作者ID': p.uid,
526
- '封面': p.cover,
527
532
  '视频链接': p.video,
528
533
  '音乐标题': p.music.title || '',
529
534
  '音乐作者': p.music.author || '',
@@ -724,113 +729,125 @@ function apply(ctx, config) {
724
729
  throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
725
730
  }
726
731
  }
727
- async function fetchApi(url, type, fieldMapping) {
728
- const cacheKey = url;
729
- const cached = urlCacheLocal.get(cacheKey);
730
- if (cached && cached.expire > Date.now())
731
- return cached.data;
732
- const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
733
- const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
734
- const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
735
- const backupAllowed = backupSupportedPlatforms.has(type);
736
- const apiList = [];
737
- if (dedicatedFirst && dedicatedUrl) {
738
- apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
739
- apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
740
- if (backupAllowed)
741
- apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
732
+ async function downloadImageFile(imageUrl) {
733
+ if (!imageUrl)
734
+ throw new Error('图片链接为空');
735
+ const imgTempDir = config.imageTempDir || './temp_images';
736
+ await promises_1.default.mkdir(imgTempDir, { recursive: true });
737
+ const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp)(\?|$)/i)?.[1] || 'jpg';
738
+ const fileName = `img_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
739
+ const filePath = path_1.default.resolve(imgTempDir, fileName);
740
+ const writer = (0, fs_1.createWriteStream)(filePath);
741
+ let response;
742
+ try {
743
+ response = await http({
744
+ method: 'GET',
745
+ url: imageUrl,
746
+ responseType: 'stream',
747
+ timeout: config.imageDownloadTimeout || 60000,
748
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
749
+ maxRedirects: 5,
750
+ validateStatus: (status) => status >= 200 && status < 300,
751
+ });
742
752
  }
743
- else {
744
- apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
745
- if (backupAllowed)
746
- apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
747
- if (dedicatedUrl)
748
- apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
753
+ catch (e) {
754
+ writer.destroy();
755
+ await promises_1.default.unlink(filePath).catch(() => { });
756
+ throw new Error(`下载图片失败: ${getErrorMessage(e)}`);
749
757
  }
750
- const customHeaders = config.customHeaders || [];
751
- let lastError = null;
752
- for (const api of apiList) {
753
- for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
758
+ const maxSizeBytes = (config.maxImageSize ?? 0) * 1024 * 1024;
759
+ const contentLength = Number(response.headers['content-length'] || 0);
760
+ if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
761
+ writer.destroy();
762
+ await promises_1.default.unlink(filePath).catch(() => { });
763
+ throw new Error(`图片文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxImageSize}MB)`);
764
+ }
765
+ try {
766
+ await (0, promises_2.pipeline)(response.data, writer);
767
+ return filePath;
768
+ }
769
+ catch (e) {
770
+ await promises_1.default.unlink(filePath).catch(() => { });
771
+ throw new Error(`写入图片文件失败: ${getErrorMessage(e)}`);
772
+ }
773
+ }
774
+ async function sendImage(session, imageUrl) {
775
+ if (!config.showCoverImage)
776
+ return;
777
+ const sendLink = async () => { await sendWithTimeout(session, `图片链接:${imageUrl}`).catch(() => { }); };
778
+ if (config.forceDownloadImage) {
779
+ try {
780
+ const localPath = await downloadImageFile(imageUrl);
781
+ await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
782
+ return;
783
+ }
784
+ catch (e) {
785
+ debugLog('ERROR', '强制下载图片失败,尝试URL发送:', getErrorMessage(e));
754
786
  try {
755
- const headers = {
756
- 'User-Agent': config.userAgent,
757
- 'Referer': 'https://www.baidu.com/',
758
- 'Content-Type': 'application/x-www-form-urlencoded'
759
- };
760
- for (const h of customHeaders) {
761
- if (h.name && h.value)
762
- headers[h.name] = h.value;
763
- }
764
- if (api.apiKey) {
765
- const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
766
- Object.assign(headers, authHeaders);
767
- }
768
- const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
769
- if (res.data && (res.data.code === 200 || res.data.code === 0)) {
770
- const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
771
- urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
772
- return parsed;
773
- }
774
- throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
787
+ await sendWithTimeout(session, koishi_1.h.image(imageUrl));
788
+ return;
775
789
  }
776
- catch (error) {
777
- lastError = error instanceof Error ? error : new Error(String(error));
778
- debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
779
- if (attempt < config.retryTimes)
780
- await delay(config.retryInterval);
790
+ catch {
791
+ await sendLink();
781
792
  }
782
793
  }
783
- debugLog('WARN', `${api.label} all retries failed`);
794
+ return;
784
795
  }
785
- throw lastError || new Error('所有API请求全部失败');
786
- }
787
- async function parseUrl(url, type, fieldMapping) {
788
- const realUrl = await resolveShortUrl(url);
789
- const candidates = [...new Set([realUrl, url])];
790
- for (const candidate of candidates) {
796
+ if (!config.showImageFile) {
797
+ await sendLink();
798
+ return;
799
+ }
800
+ try {
801
+ await sendWithTimeout(session, koishi_1.h.image(imageUrl));
802
+ }
803
+ catch {
791
804
  try {
792
- const info = await fetchApi(candidate, type, fieldMapping);
793
- if (info.video || info.images.length > 0 || info.live_photo.length > 0)
794
- return { success: true, data: info };
795
- debugLog('WARN', `解析成功但无内容: ${candidate}`);
805
+ const localPath = await downloadImageFile(imageUrl);
806
+ await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
796
807
  }
797
- catch (error) {
798
- debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
808
+ catch {
809
+ await sendLink();
799
810
  }
800
811
  }
801
- return { success: false, msg: texts.unsupportedPlatformText };
802
812
  }
803
- async function processSingleUrl(url, type, fieldMapping) {
804
- const result = await parseUrl(url, type, fieldMapping);
805
- if (!result.success)
806
- return { success: false, msg: result.msg, url };
807
- const text = generateFormattedText(result.data, config.unifiedMessageFormat);
808
- return { success: true, data: { text, parsed: result.data } };
809
- }
810
- async function sendWithTimeout(session, content, customRetries) {
811
- const maxRetries = customRetries ?? config.retryTimes ?? 3;
812
- const retryDelay = config.retryInterval || 1000;
813
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
813
+ async function sendMusicCover(session, imageUrl) {
814
+ if (!config.showMusicCover)
815
+ return;
816
+ const sendLink = async () => { await sendWithTimeout(session, `图片链接:${imageUrl}`).catch(() => { }); };
817
+ if (config.forceDownloadImage) {
814
818
  try {
815
- let sendPromise = session.send(content);
816
- if (config.videoSendTimeout > 0) {
817
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout));
818
- return await Promise.race([sendPromise, timeoutPromise]);
819
+ const localPath = await downloadImageFile(imageUrl);
820
+ await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
821
+ return;
822
+ }
823
+ catch (e) {
824
+ debugLog('ERROR', '强制下载音乐封面失败,尝试URL发送:', getErrorMessage(e));
825
+ try {
826
+ await sendWithTimeout(session, koishi_1.h.image(imageUrl));
827
+ return;
819
828
  }
820
- else {
821
- return await sendPromise;
829
+ catch {
830
+ await sendLink();
822
831
  }
823
832
  }
824
- catch (err) {
825
- const errMsg = getErrorMessage(err);
826
- debugLog('ERROR', `发送失败尝试 ${attempt + 1}: ${errMsg}`);
827
- if (attempt < maxRetries)
828
- await delay(retryDelay);
829
- else if (!config.ignoreSendError)
830
- throw err;
833
+ return;
834
+ }
835
+ if (!config.showImageFile) {
836
+ await sendLink();
837
+ return;
838
+ }
839
+ try {
840
+ await sendWithTimeout(session, koishi_1.h.image(imageUrl));
841
+ }
842
+ catch {
843
+ try {
844
+ const localPath = await downloadImageFile(imageUrl);
845
+ await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
846
+ }
847
+ catch {
848
+ await sendLink();
831
849
  }
832
850
  }
833
- return null;
834
851
  }
835
852
  async function sendVideoFile(session, videoUrl) {
836
853
  if (!videoUrl)
@@ -845,7 +862,7 @@ function apply(ctx, config) {
845
862
  return;
846
863
  }
847
864
  catch (e) {
848
- debugLog('ERROR', '强制下载失败,尝试URL发送:', getErrorMessage(e));
865
+ debugLog('ERROR', '强制下载视频失败,尝试URL发送:', getErrorMessage(e));
849
866
  try {
850
867
  await sendWithTimeout(session, koishi_1.h.video(videoUrl));
851
868
  return;
@@ -917,8 +934,12 @@ function apply(ctx, config) {
917
934
  const text = item.text;
918
935
  if (text && config.showImageText)
919
936
  forwardMessages.push(buildForwardNode(session, text, botName));
920
- if (config.showCoverImage && p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length)))
937
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
921
938
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
939
+ }
940
+ if (config.showMusicCover && p.music.cover) {
941
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.music.cover), botName));
942
+ }
922
943
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
923
944
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
924
945
  for (const imgUrl of imageUrls)
@@ -948,21 +969,22 @@ function apply(ctx, config) {
948
969
  await sendWithTimeout(session, text);
949
970
  await delay(300);
950
971
  }
951
- if (config.showCoverImage && p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
952
- await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
972
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
973
+ await sendImage(session, p.cover).catch(() => { });
974
+ await delay(300);
975
+ }
976
+ if (config.showMusicCover && p.music.cover) {
977
+ await sendMusicCover(session, p.music.cover).catch(() => { });
953
978
  await delay(300);
954
979
  }
955
980
  if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
956
- if (config.showVideoFile)
957
- await sendVideoFile(session, p.video);
958
- else
959
- await sendWithTimeout(session, `视频链接:${p.video}`);
981
+ await sendVideoFile(session, p.video);
960
982
  await delay(500);
961
983
  }
962
984
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
963
985
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
964
986
  for (const imgUrl of imageUrls) {
965
- await sendWithTimeout(session, koishi_1.h.image(imgUrl)).catch(() => { });
987
+ await sendImage(session, imgUrl).catch(() => { });
966
988
  await delay(200);
967
989
  }
968
990
  }
@@ -970,6 +992,114 @@ function apply(ctx, config) {
970
992
  }
971
993
  debugLog('INFO', '处理完成');
972
994
  }
995
+ async function fetchApi(url, type, fieldMapping) {
996
+ const cacheKey = url;
997
+ const cached = urlCacheLocal.get(cacheKey);
998
+ if (cached && cached.expire > Date.now())
999
+ return cached.data;
1000
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
1001
+ const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
1002
+ const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
1003
+ const backupAllowed = backupSupportedPlatforms.has(type);
1004
+ const apiList = [];
1005
+ if (dedicatedFirst && dedicatedUrl) {
1006
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
1007
+ apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
1008
+ if (backupAllowed)
1009
+ apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
1010
+ }
1011
+ else {
1012
+ apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
1013
+ if (backupAllowed)
1014
+ apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
1015
+ if (dedicatedUrl)
1016
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
1017
+ }
1018
+ const customHeaders = config.customHeaders || [];
1019
+ let lastError = null;
1020
+ for (const api of apiList) {
1021
+ for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
1022
+ try {
1023
+ const headers = {
1024
+ 'User-Agent': config.userAgent,
1025
+ 'Referer': 'https://www.baidu.com/',
1026
+ 'Content-Type': 'application/x-www-form-urlencoded'
1027
+ };
1028
+ for (const h of customHeaders) {
1029
+ if (h.name && h.value)
1030
+ headers[h.name] = h.value;
1031
+ }
1032
+ if (api.apiKey) {
1033
+ const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
1034
+ Object.assign(headers, authHeaders);
1035
+ }
1036
+ const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
1037
+ if (res.data && (res.data.code === 200 || res.data.code === 0)) {
1038
+ const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
1039
+ urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
1040
+ return parsed;
1041
+ }
1042
+ throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
1043
+ }
1044
+ catch (error) {
1045
+ lastError = error instanceof Error ? error : new Error(String(error));
1046
+ debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
1047
+ if (attempt < config.retryTimes)
1048
+ await delay(config.retryInterval);
1049
+ }
1050
+ }
1051
+ debugLog('WARN', `${api.label} all retries failed`);
1052
+ }
1053
+ throw lastError || new Error('所有API请求全部失败');
1054
+ }
1055
+ async function parseUrl(url, type, fieldMapping) {
1056
+ const realUrl = await resolveShortUrl(url);
1057
+ const candidates = [...new Set([realUrl, url])];
1058
+ for (const candidate of candidates) {
1059
+ try {
1060
+ const info = await fetchApi(candidate, type, fieldMapping);
1061
+ if (info.video || info.images.length > 0 || info.live_photo.length > 0)
1062
+ return { success: true, data: info };
1063
+ debugLog('WARN', `解析成功但无内容: ${candidate}`);
1064
+ }
1065
+ catch (error) {
1066
+ debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
1067
+ }
1068
+ }
1069
+ return { success: false, msg: texts.unsupportedPlatformText };
1070
+ }
1071
+ async function processSingleUrl(url, type, fieldMapping) {
1072
+ const result = await parseUrl(url, type, fieldMapping);
1073
+ if (!result.success)
1074
+ return { success: false, msg: result.msg, url };
1075
+ const text = generateFormattedText(result.data, config.unifiedMessageFormat);
1076
+ return { success: true, data: { text, parsed: result.data } };
1077
+ }
1078
+ async function sendWithTimeout(session, content, customRetries) {
1079
+ const maxRetries = customRetries ?? config.retryTimes ?? 3;
1080
+ const retryDelay = config.retryInterval || 1000;
1081
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1082
+ try {
1083
+ let sendPromise = session.send(content);
1084
+ if (config.videoSendTimeout > 0) {
1085
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout));
1086
+ return await Promise.race([sendPromise, timeoutPromise]);
1087
+ }
1088
+ else {
1089
+ return await sendPromise;
1090
+ }
1091
+ }
1092
+ catch (err) {
1093
+ const errMsg = getErrorMessage(err);
1094
+ debugLog('ERROR', `发送失败尝试 ${attempt + 1}: ${errMsg}`);
1095
+ if (attempt < maxRetries)
1096
+ await delay(retryDelay);
1097
+ else if (!config.ignoreSendError)
1098
+ throw err;
1099
+ }
1100
+ }
1101
+ return null;
1102
+ }
973
1103
  ctx.on('message', async (session) => {
974
1104
  if (!config.enable)
975
1105
  return;
@@ -1013,22 +1143,20 @@ function apply(ctx, config) {
1013
1143
  });
1014
1144
  const tempCleanupInterval = setInterval(async () => {
1015
1145
  try {
1016
- const tempDir = config.tempDir || './temp_videos';
1017
- const files = await promises_1.default.readdir(tempDir);
1018
- const now = Date.now();
1019
- let deleted = 0;
1020
- for (const file of files) {
1021
- if (file.startsWith('video_') && file.endsWith('.mp4')) {
1022
- const filePath = path_1.default.join(tempDir, file);
1023
- const stats = await promises_1.default.stat(filePath);
1024
- if (now - stats.mtimeMs > 3600000) {
1025
- await promises_1.default.unlink(filePath).catch(() => { });
1026
- deleted++;
1146
+ const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1147
+ for (const dir of dirs) {
1148
+ const files = await promises_1.default.readdir(dir);
1149
+ const now = Date.now();
1150
+ for (const file of files) {
1151
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1152
+ const filePath = path_1.default.join(dir, file);
1153
+ const stats = await promises_1.default.stat(filePath);
1154
+ if (now - stats.mtimeMs > 3600000) {
1155
+ await promises_1.default.unlink(filePath).catch(() => { });
1156
+ }
1027
1157
  }
1028
1158
  }
1029
1159
  }
1030
- if (deleted)
1031
- debugLog('INFO', `清理了 ${deleted} 个过期临时视频文件`);
1032
1160
  }
1033
1161
  catch (e) {
1034
1162
  debugLog('WARN', '清理临时文件失败:', e);
@@ -1042,11 +1170,14 @@ function apply(ctx, config) {
1042
1170
  });
1043
1171
  process.on('beforeExit', async () => {
1044
1172
  try {
1045
- const tempDir = config.tempDir || './temp_videos';
1046
- const files = await promises_1.default.readdir(tempDir);
1047
- for (const file of files) {
1048
- if (file.startsWith('video_') && file.endsWith('.mp4'))
1049
- await promises_1.default.unlink(path_1.default.join(tempDir, file)).catch(() => { });
1173
+ const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1174
+ for (const dir of dirs) {
1175
+ const files = await promises_1.default.readdir(dir);
1176
+ for (const file of files) {
1177
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1178
+ await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
1179
+ }
1180
+ }
1050
1181
  }
1051
1182
  }
1052
1183
  catch { }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
- "description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
4
- "version": "1.3.2",
3
+ "description": "Koishi 全平台视频/图集解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
4
+ "version": "1.3.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -45,10 +45,45 @@
45
45
  "twitter",
46
46
  "instagram",
47
47
  "doubao",
48
+ "doubao_chat",
48
49
  "jimeng",
50
+ "oasis",
51
+ "wechat_channel",
49
52
  "debug",
50
53
  "unified-api",
51
- "视频解析"
54
+ "proxy",
55
+ "field-mapping",
56
+ "image-download",
57
+ "视频解析",
58
+ "多平台",
59
+ "图集解析",
60
+ "去水印",
61
+ "哔哩哔哩",
62
+ "抖音",
63
+ "快手",
64
+ "微博",
65
+ "头条",
66
+ "西瓜视频",
67
+ "小红书",
68
+ "剪映",
69
+ "A站",
70
+ "知乎",
71
+ "微视",
72
+ "虎牙",
73
+ "油管",
74
+ "国际版抖音",
75
+ "好看视频",
76
+ "美拍",
77
+ "全民直播",
78
+ "推特",
79
+ "照片墙",
80
+ "豆包",
81
+ "即梦",
82
+ "绿洲",
83
+ "视频号",
84
+ "皮皮搞笑",
85
+ "皮皮虾",
86
+ "最右"
52
87
  ],
53
88
  "devDependencies": {
54
89
  "@koishijs/client": "^5.30.4",
@@ -58,8 +93,7 @@
58
93
  },
59
94
  "dependencies": {
60
95
  "axios": "^1.16.1",
61
- "fast-xml-parser": "^4.5.6",
62
- "stream": "^0.0.3"
96
+ "fast-xml-parser": "^4.5.6"
63
97
  },
64
98
  "peerDependencies": {
65
99
  "@koishijs/plugin-console": "^5.30.4",
@@ -76,4 +110,4 @@
76
110
  "engines": {
77
111
  "node": ">=16.0.0"
78
112
  }
79
- }
113
+ }
package/readme.md CHANGED
@@ -31,95 +31,100 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
31
31
  ### 统一消息格式 (Unified Message Format)
32
32
  | 配置项 | 类型 | 默认值 | 说明 |
33
33
  |--------|------|--------|------|
34
- | `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n音乐封面:${音乐封面}\n音乐链接:${音乐链接}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 自定义解析结果的输出格式,支持变量替换。某行所有变量均为空(或为"0")时自动隐藏该行 |
34
+ | `unifiedMessageFormat` | string | `标题:${标题}\n作者:${作者}\n简介:${简介}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n音乐链接:${音乐链接}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}` | 文字消息格式,支持变量替换。空行自动隐藏。封面及媒体由独立开关控制,默认不包含在文字中 |
35
35
 
36
36
  ### 内容显示设置 (Content Display Settings)
37
37
  | 配置项 | 类型 | 默认值 | 说明 |
38
38
  |--------|------|--------|------|
39
- | `showImageText` | boolean | true | 是否发送解析后的文字内容 |
40
- | `showCoverImage` | boolean | true | 是否发送封面图片(关闭后不再自动发送封面) |
41
- | `showVideoFile` | boolean | true | 是否发送视频文件(关闭则只发送视频链接) |
42
- | `maxDescLength` | number | 200 | 简介内容最大长度(字符),超出自动截断 |
39
+ | `showImageText` | boolean | true | 是否发送文字内容 |
40
+ | `showCoverImage` | boolean | true | 是否发送封面图片(视频/图集封面) |
41
+ | `showMusicCover` | boolean | true | 是否发送音乐封面图片 |
42
+ | `showImageFile` | boolean | true | 封面/图片是否以文件形式发送(关闭则只发链接) |
43
+ | `forceDownloadImage` | boolean | false | 强制下载封面/图片后发送 |
44
+ | `imageDownloadTimeout` | number | 60000 | 图片下载超时(毫秒) |
45
+ | `imageTempDir` | string | `./temp_images` | 临时封面/图片存储目录 |
46
+ | `maxImageSize` | number | 0 | 最大下载图片大小(MB),0 不限制 |
47
+ | `showVideoFile` | boolean | true | 视频是否以文件形式发送(关闭则只发链接) |
48
+ | `forceDownloadVideo` | boolean | false | 强制下载视频后发送 |
43
49
  | `videoDownloadTimeout` | number | 120000 | 视频下载超时(毫秒) |
44
50
  | `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
45
- | `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 为不限制大小 |
46
- | `forceDownloadVideo` | boolean | false | 强制下载视频后发送 |
47
- | `maxConcurrent` | number | 3 | 批量解析时最大并发数,避免同时下载过多 |
51
+ | `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 不限制 |
52
+ | `maxDescLength` | number | 200 | 简介最大长度(字符) |
53
+ | `maxConcurrent` | number | 3 | 批量解析最大并发数 |
48
54
 
49
55
  ### 网络与 API 设置 (Network & API Settings)
50
56
  | 配置项 | 类型 | 默认值 | 说明 |
51
57
  |--------|------|--------|------|
52
58
  | `timeout` | number | 180000 | API 请求超时时间(毫秒) |
53
- | `videoSendTimeout` | number | 60000 | 视频消息发送超时时间(毫秒,0 为不限制) |
54
- | `userAgent` | string | `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36` | API 请求使用的 User-Agent |
55
- | `proxy` | object | `{ enabled: false, protocol: "http", host: "127.0.0.1", port: 7890, auth: { username: "", password: "" } }` | HTTP/HTTPS 代理设置。`enabled` 开关(默认关闭),`protocol` 下拉选择 `http` 或 `https`。需开启 `enabled` 并填写 `host` 后生效 |
56
- | `customHeaders` | array | [] | 自定义请求头,会附加到所有 API 请求中。每项包含 `name`(头名称)和 `value`(头值) |
59
+ | `videoSendTimeout` | number | 60000 | 消息发送超时时间(毫秒,0 为不限制) |
60
+ | `userAgent` | string | `Mozilla/5.0 ...` | User-Agent |
61
+ | `proxy` | object | `{ enabled: false, protocol: "http", host: "127.0.0.1", port: 7890, auth: { username: "", password: "" } }` | HTTP/HTTPS 代理。`enabled` 开关(默认关闭),`protocol` 下拉选择 `http` 或 `https` |
62
+ | `customHeaders` | array | [] | 自定义请求头,每项含 `name` `value` |
57
63
 
58
64
  ### API 选择与回退设置 (API Selection & Fallback)
59
65
  | 配置项 | 类型 | 默认值 | 说明 |
60
66
  |--------|------|--------|------|
61
- | `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址,解析时优先使用 |
62
- | `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API 地址,仅支持抖音、小红书、Instagram、即梦平台解析 |
63
- | `platformDedicatedFirst` | object | 各平台均为 `false` | 各平台独立开关:是否优先使用平台专属 API。对象键为平台标识(英文),值为布尔值。支持的键:`bilibili`、`douyin`、`kuaishou`、`xiaohongshu`、`weibo`、`xigua`、`youtube`、`tiktok`、`acfun`、`zhihu`、`weishi`、`huya`、`haokan`、`meipai`、`twitter`、`instagram`、`doubao`、`doubao_chat`、`oasis`、`wechat_channel` |
64
- | `customApis` | array | [] | 自定义平台专属 API 列表。每项包含:`platform`(平台类型)、`apiUrl`(API 地址)、`apiKey`(API Key,可选)、`authHeaderType`(认证头类型,可选:`Bearer` / `X-API-Key` / `Custom`)、`customHeaderName`(自定义 Header 名称,仅当 `authHeaderType` 为 `Custom` 时有效)、`fieldMapping`(字段映射 JSON 字符串,用于适配非标准 API 响应,支持点号路径) |
65
- | `globalFieldMapping` | string | 预设完整字段映射JSON(见下方示例) | 全局字段映射 JSON,优先级低于专属 API 映射。用于统一适配所有平台的 API 响应格式,默认已包含常用路径示例 |
67
+ | `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址 |
68
+ | `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API,仅支持部分平台 |
69
+ | `platformDedicatedFirst` | object | 各平台均为 `false` | 平台专属 API 优先开关,键:`bilibili` |
70
+ | `customApis` | array | [] | 自定义平台专属 API,含 `platform`, `apiUrl`, `apiKey`, `authHeaderType`, `customHeaderName`, `fieldMapping` |
71
+ | `globalFieldMapping` | string | 预设字段映射 JSON | 全局字段映射,支持点号路径 |
66
72
 
67
73
  ### 错误与重试设置 (Error & Retry Settings)
68
74
  | 配置项 | 类型 | 默认值 | 说明 |
69
75
  |--------|------|--------|------|
70
- | `ignoreSendError` | boolean | true | 是否忽略消息发送失败,避免插件崩溃 |
71
- | `retryTimes` | number | 3 | API 请求及消息发送失败时的重试次数 |
72
- | `retryInterval` | number | 1000 | API 请求及消息发送重试的间隔时间(毫秒) |
76
+ | `ignoreSendError` | boolean | true | 忽略发送失败 |
77
+ | `retryTimes` | number | 3 | 重试次数 |
78
+ | `retryInterval` | number | 1000 | 重试间隔(毫秒) |
73
79
 
74
80
  ### 发送方式设置 (Send Mode Settings)
75
81
  | 配置项 | 类型 | 默认值 | 说明 |
76
82
  |--------|------|--------|------|
77
- | `enableForward` | boolean | false | 是否启用合并转发(仅 OneBot 平台),启用后视频与图文将整合进同一条合并消息 |
83
+ | `enableForward` | boolean | false | 启用合并转发(仅 OneBot 平台) |
78
84
 
79
85
  ### 缓存与去重设置 (Cache & Deduplication Settings)
80
86
  | 配置项 | 类型 | 默认值 | 说明 |
81
87
  |--------|------|--------|------|
82
- | `deduplicationInterval` | number | 180 | 禁止重复解析时间间隔(秒),0 为不限制。同一个链接在间隔内不会重复解析。 |
83
- | `cacheTTL` | number | 600 | 解析结果缓存时间(秒),0 为不缓存。缓存可减少重复 API 请求。 |
88
+ | `deduplicationInterval` | number | 180 | 去重间隔(秒) |
89
+ | `cacheTTL` | number | 600 | 缓存时间(秒) |
84
90
 
85
91
  ### 界面文字设置 (UI Text Settings)
86
92
  | 配置项 | 类型 | 默认值 | 说明 |
87
93
  |--------|------|--------|------|
88
- | `waitingTipText` | string | 正在解析视频,请稍候... | 解析等待提示文字 |
89
- | `unsupportedPlatformText` | string | 不支持该平台链接 | 不支持的平台提示 |
90
- | `invalidLinkText` | string | 无效的视频链接 | 无效链接提示(parse 指令) |
91
- | `parseErrorPrefix` | string | ❌ 解析失败: | 解析失败消息前缀 |
92
- | `parseErrorItemFormat` | string | `【${url}】: ${msg}` | 每条解析失败的展示格式,可用 ${url}(链接)和 ${msg}(错误信息) |
94
+ | `waitingTipText` | string | 正在解析视频,请稍候... | 等待提示 |
95
+ | `unsupportedPlatformText` | string | 不支持该平台链接 | 不支持平台提示 |
96
+ | `invalidLinkText` | string | 无效的视频链接 | 无效链接提示 |
97
+ | `parseErrorPrefix` | string | ❌ 解析失败: | 错误前缀 |
98
+ | `parseErrorItemFormat` | string | `【${url}】: ${msg}` | 错误项格式 |
93
99
 
94
100
  ## 支持的变量 (Supported Variables)
95
- 在 `unifiedMessageFormat` 中可使用以下变量进行自定义格式化,某行所有变量均为空(或为"0")时该行不显示:
96
-
97
- | 变量名 | 说明 | 适用平台 |
98
- |--------|------|----------|
99
- | `${标题}` | 视频/图集标题 | 所有平台 |
100
- | `${作者}` | 作者/发布者名称 | 所有平台 |
101
- | `${简介}` | 内容简介/描述 | 所有平台 |
102
- | `${视频时长}` | 视频时长(时:分:秒) | 视频 |
103
- | `${点赞数}` | 点赞数量 | 所有平台 |
104
- | `${收藏数}` | 收藏数量 | 所有平台 |
105
- | `${转发数}` | 转发/分享数量 | 所有平台 |
106
- | `${播放数}` | 播放量 | 部分平台 |
107
- | `${评论数}` | 评论数量 | 所有平台 |
108
- | `${发布时间}` | 发布时间(格式化) | 所有平台 |
109
- | `${图片数量}` | 图集/实况图片数量 | 图集/实况 |
110
- | `${作者ID}` | 作者唯一标识ID | 部分平台 |
111
- | `${视频链接}` | 视频原始链接 | 视频 |
112
- | `${音乐标题}` | 音乐标题 | 部分平台 |
113
- | `${音乐作者}` | 音乐作者 | 部分平台 |
114
- | `${音乐封面}` | 音乐封面图片地址 | 部分平台 |
115
- | `${音乐链接}` | 音乐原始链接 | 部分平台 |
116
-
117
- > 注:部分变量可能因平台API返回数据不同而显示为空,某行所有变量为空(或为"0")时该行会自动隐藏。
101
+ 在 `unifiedMessageFormat` 中可使用以下变量,空行自动隐藏:
102
+
103
+ | 变量名 | 说明 |
104
+ |--------|------|
105
+ | `${标题}` | 视频/图集标题 |
106
+ | `${作者}` | 作者名称 |
107
+ | `${简介}` | 内容简介 |
108
+ | `${视频时长}` | 视频时长(时:分:秒) |
109
+ | `${点赞数}` | 点赞数量 |
110
+ | `${收藏数}` | 收藏数量 |
111
+ | `${转发数}` | 转发/分享数量 |
112
+ | `${播放数}` | 播放量 |
113
+ | `${评论数}` | 评论数量 |
114
+ | `${发布时间}` | 发布时间(格式化) |
115
+ | `${图片数量}` | 图集/实况图片数量 |
116
+ | `${作者ID}` | 作者唯一标识ID |
117
+ | `${视频链接}` | 视频原始链接 |
118
+ | `${音乐标题}` | 音乐标题 |
119
+ | `${音乐作者}` | 音乐作者 |
120
+ | `${音乐链接}` | 音乐原始链接 |
121
+
122
+ > 注:音乐封面已转为独立图片发送,不再作为文字变量。其余封面图片均通过对应开关控制。
118
123
 
119
124
  ## 支持的平台 (Supported Platforms)
120
125
  | 平台名称 | 关键词识别 | 解析能力 |
121
126
  |----------|------------|----------|
122
- | 哔哩哔哩 (B站) | bilibili, b23.tv, bilibili.com | 视频(不含番剧/直播/图文) |
127
+ | 哔哩哔哩 (B站) | bilibili, b23.tv, bilibili.com | 视频 |
123
128
  | 抖音 | douyin, v.douyin.com | 短视频、图集、实况 |
124
129
  | 快手 | kuaishou, v.kuaishou.com | 短视频、图集 |
125
130
  | 小红书 | xiaohongshu, xhslink.com | 图文、视频 |
@@ -146,7 +151,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
146
151
  | 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
147
152
  | 视频号 (WeChat Channels) | channels.weixin.qq.com, weixin.qq.com/sph/ | 短视频 |
148
153
 
149
- > 注:部分平台解析能力可能因API限制有所差异,具体以实际解析结果为准。
154
+ > 注:部分平台解析能力可能因API限制有所差异。
150
155
 
151
156
  ## 项目贡献者 (Contributors)
152
157
 
@@ -170,6 +175,6 @@ This project is licensed under the MIT License, see the [LICENSE](LICENSE) file
170
175
 
171
176
  ## 支持我们 (Support Us)
172
177
 
173
- 如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们,这将是对所有贡献者最大的鼓励!
178
+ 如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们!
174
179
 
175
- If this project is helpful to you, please feel free to star it in the upper right corner ⭐ to support us, which will be the greatest encouragement to all contributors!
180
+ If this project is helpful to you, please feel free to star it in the upper right corner ⭐ to support us!