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

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,17 @@ export declare const Config: Schema<{
10
10
  } & {
11
11
  showImageText?: boolean | null | undefined;
12
12
  showCoverImage?: boolean | null | undefined;
13
+ showImageFile?: boolean | null | undefined;
14
+ forceDownloadImage?: boolean | null | undefined;
15
+ imageDownloadTimeout?: number | null | undefined;
16
+ imageTempDir?: string | null | undefined;
17
+ maxImageSize?: number | null | undefined;
13
18
  showVideoFile?: boolean | null | undefined;
14
- maxDescLength?: number | null | undefined;
19
+ forceDownloadVideo?: boolean | null | undefined;
15
20
  videoDownloadTimeout?: number | null | undefined;
16
21
  tempDir?: string | null | undefined;
17
22
  maxVideoSize?: number | null | undefined;
18
- forceDownloadVideo?: boolean | null | undefined;
23
+ maxDescLength?: number | null | undefined;
19
24
  maxConcurrent?: number | null | undefined;
20
25
  } & {
21
26
  timeout?: number | null | undefined;
@@ -94,12 +99,17 @@ export declare const Config: Schema<{
94
99
  } & {
95
100
  showImageText: boolean;
96
101
  showCoverImage: boolean;
102
+ showImageFile: boolean;
103
+ forceDownloadImage: boolean;
104
+ imageDownloadTimeout: number;
105
+ imageTempDir: string;
106
+ maxImageSize: number;
97
107
  showVideoFile: boolean;
98
- maxDescLength: number;
108
+ forceDownloadVideo: boolean;
99
109
  videoDownloadTimeout: number;
100
110
  tempDir: string;
101
111
  maxVideoSize: number;
102
- forceDownloadVideo: boolean;
112
+ maxDescLength: number;
103
113
  maxConcurrent: number;
104
114
  } & {
105
115
  timeout: number;
package/lib/index.js CHANGED
@@ -76,78 +76,83 @@ 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评论:${评论数}\n图片数量:${图片数量}').description('文字消息格式,支持变量。某行所有变量为空时自动隐藏。封面及媒体文件由独立开关控制,默认不包含在文字中'),
80
80
  }).description('消息格式设置'),
81
81
  koishi_1.Schema.object({
82
- showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
82
+ showImageText: koishi_1.Schema.boolean().default(true).description('是否发送文字内容'),
83
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('简介内容最大长度(字符),超出自动截断'),
84
+ showImageFile: koishi_1.Schema.boolean().default(true).description('封面/图片是否以文件形式发送(关闭则只发送链接)'),
85
+ forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面/图片后发送'),
86
+ imageDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('图片下载超时(毫秒)'),
87
+ imageTempDir: koishi_1.Schema.string().default('./temp_images').description('临时封面/图片存储目录'),
88
+ maxImageSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载图片大小(MB),0 为不限制'),
89
+ showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以文件形式发送(关闭则只发送链接)'),
90
+ forceDownloadVideo: koishi_1.Schema.boolean().default(false).description('强制下载视频后发送'),
86
91
  videoDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('视频下载超时(毫秒)'),
87
92
  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('批量解析时最大并发数'),
93
+ maxVideoSize: koishi_1.Schema.number().min(0).step(1).default(0).description('最大下载视频大小(MB),0 为不限制'),
94
+ maxDescLength: koishi_1.Schema.number().min(0).step(1).default(200).description('简介最大长度(字符)'),
95
+ maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('批量解析最大并发数'),
91
96
  }).description('内容显示设置'),
92
97
  koishi_1.Schema.object({
93
98
  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'),
99
+ videoSendTimeout: koishi_1.Schema.number().min(0).step(1).default(60000).description('消息发送超时(毫秒)'),
100
+ 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
101
  proxy: koishi_1.Schema.object({
97
- enabled: koishi_1.Schema.boolean().default(false).description('是否启用 HTTP/HTTPS 代理'),
102
+ enabled: koishi_1.Schema.boolean().default(false).description('启用代理'),
98
103
  protocol: koishi_1.Schema.union([
99
104
  koishi_1.Schema.const('http').description('HTTP'),
100
105
  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('代理端口'),
106
+ ]).default('http').description('协议'),
107
+ host: koishi_1.Schema.string().default('127.0.0.1').description('地址'),
108
+ port: koishi_1.Schema.number().default(7890).description('端口'),
104
109
  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)'),
110
+ username: koishi_1.Schema.string().default('').description('用户名'),
111
+ password: koishi_1.Schema.string().default('').description('密码'),
112
+ }).description('认证'),
113
+ }).description('HTTP/HTTPS 代理'),
109
114
  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 设置'),
115
+ name: koishi_1.Schema.string().required().description('头名称'),
116
+ value: koishi_1.Schema.string().required().description('头值'),
117
+ })).default([]).description('自定义请求头'),
118
+ }).description('网络设置'),
114
119
  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('错误与重试设置'),
120
+ ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败'),
121
+ retryTimes: koishi_1.Schema.number().min(0).step(1).default(3).description('重试次数'),
122
+ retryInterval: koishi_1.Schema.number().min(0).step(1).default(1000).description('重试间隔(毫秒)'),
123
+ }).description('错误与重试'),
119
124
  koishi_1.Schema.object({
120
- enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
121
- }).description('发送方式设置'),
125
+ enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot'),
126
+ }).description('发送方式'),
122
127
  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('缓存与去重设置'),
128
+ deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('去重间隔(秒)'),
129
+ cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间(秒)'),
130
+ }).description('缓存与去重'),
126
131
  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 地址'),
132
+ primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API'),
133
+ backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API'),
129
134
  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'),
135
+ bilibili: koishi_1.Schema.boolean().default(false).description('哔哩哔哩'),
136
+ douyin: koishi_1.Schema.boolean().default(false).description('抖音'),
137
+ kuaishou: koishi_1.Schema.boolean().default(false).description('快手'),
138
+ xiaohongshu: koishi_1.Schema.boolean().default(false).description('小红书'),
139
+ weibo: koishi_1.Schema.boolean().default(false).description('微博'),
140
+ xigua: koishi_1.Schema.boolean().default(false).description('西瓜视频'),
141
+ youtube: koishi_1.Schema.boolean().default(false).description('YouTube'),
142
+ tiktok: koishi_1.Schema.boolean().default(false).description('TikTok'),
143
+ acfun: koishi_1.Schema.boolean().default(false).description('AcFun'),
144
+ zhihu: koishi_1.Schema.boolean().default(false).description('知乎'),
145
+ weishi: koishi_1.Schema.boolean().default(false).description('微视'),
146
+ huya: koishi_1.Schema.boolean().default(false).description('虎牙'),
147
+ haokan: koishi_1.Schema.boolean().default(false).description('好看视频'),
148
+ meipai: koishi_1.Schema.boolean().default(false).description('美拍'),
149
+ twitter: koishi_1.Schema.boolean().default(false).description('Twitter/X'),
150
+ instagram: koishi_1.Schema.boolean().default(false).description('Instagram'),
151
+ doubao: koishi_1.Schema.boolean().default(false).description('豆包'),
152
+ doubao_chat: koishi_1.Schema.boolean().default(false).description('豆包对话'),
153
+ oasis: koishi_1.Schema.boolean().default(false).description('绿洲'),
154
+ wechat_channel: koishi_1.Schema.boolean().default(false).description('视频号'),
155
+ }).description('优先使用专属 API'),
151
156
  customApis: koishi_1.Schema.array(koishi_1.Schema.object({
152
157
  platform: koishi_1.Schema.union([
153
158
  koishi_1.Schema.const('bilibili').description('哔哩哔哩'),
@@ -170,17 +175,17 @@ exports.Config = koishi_1.Schema.intersect([
170
175
  koishi_1.Schema.const('doubao_chat').description('豆包对话'),
171
176
  koishi_1.Schema.const('oasis').description('绿洲'),
172
177
  koishi_1.Schema.const('wechat_channel').description('视频号'),
173
- ]).description('选择平台'),
178
+ ]).description('平台'),
174
179
  apiUrl: koishi_1.Schema.string().description('API 地址'),
175
- apiKey: koishi_1.Schema.string().description('API Key(可选)').default(''),
180
+ apiKey: koishi_1.Schema.string().description('API Key').default(''),
176
181
  authHeaderType: koishi_1.Schema.union([
177
- koishi_1.Schema.const('Bearer').description('Bearer Token'),
182
+ koishi_1.Schema.const('Bearer').description('Bearer'),
178
183
  koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
179
- koishi_1.Schema.const('Custom').description('自定义 Header 名称'),
184
+ koishi_1.Schema.const('Custom').description('自定义'),
180
185
  ]).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'),
186
+ customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
187
+ fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
188
+ })).default([]).description('自定义专属 API'),
184
189
  globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
185
190
  ' "title": "data.title",\n' +
186
191
  ' "desc": "data.description",\n' +
@@ -203,15 +208,15 @@ exports.Config = koishi_1.Schema.intersect([
203
208
  ' "music_author": "data.music.author",\n' +
204
209
  ' "music_cover": "data.music.cover",\n' +
205
210
  ' "music_url": "data.music.url"\n' +
206
- '}').description('全局字段映射 JSON,优先级低于专属 API 映射'),
207
- }).description('API 选择设置'),
211
+ '}').description('全局字段映射 JSON'),
212
+ }).description('API 选择'),
208
213
  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('界面文字设置'),
214
+ waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示'),
215
+ unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持提示'),
216
+ invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示'),
217
+ parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('错误前缀'),
218
+ parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('错误项格式'),
219
+ }).description('界面文字'),
215
220
  ]);
216
221
  const logger = new koishi_1.Logger(exports.name);
217
222
  let debugEnabled = false;
@@ -523,7 +528,6 @@ function generateFormattedText(p, format) {
523
528
  '发布时间': p.publishTime ? formatPublishTime(p.publishTime) : '',
524
529
  '图片数量': String(imageCount),
525
530
  '作者ID': p.uid,
526
- '封面': p.cover,
527
531
  '视频链接': p.video,
528
532
  '音乐标题': p.music.title || '',
529
533
  '音乐作者': p.music.author || '',
@@ -724,113 +728,86 @@ function apply(ctx, config) {
724
728
  throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
725
729
  }
726
730
  }
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 });
731
+ async function downloadImageFile(imageUrl) {
732
+ if (!imageUrl)
733
+ throw new Error('图片链接为空');
734
+ const imgTempDir = config.imageTempDir || './temp_images';
735
+ await promises_1.default.mkdir(imgTempDir, { recursive: true });
736
+ const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp)(\?|$)/i)?.[1] || 'jpg';
737
+ const fileName = `img_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
738
+ const filePath = path_1.default.resolve(imgTempDir, fileName);
739
+ const writer = (0, fs_1.createWriteStream)(filePath);
740
+ let response;
741
+ try {
742
+ response = await http({
743
+ method: 'GET',
744
+ url: imageUrl,
745
+ responseType: 'stream',
746
+ timeout: config.imageDownloadTimeout || 60000,
747
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.baidu.com/' },
748
+ maxRedirects: 5,
749
+ validateStatus: (status) => status >= 200 && status < 300,
750
+ });
742
751
  }
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 });
752
+ catch (e) {
753
+ writer.destroy();
754
+ await promises_1.default.unlink(filePath).catch(() => { });
755
+ throw new Error(`下载图片失败: ${getErrorMessage(e)}`);
749
756
  }
750
- const customHeaders = config.customHeaders || [];
751
- let lastError = null;
752
- for (const api of apiList) {
753
- for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
754
- 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}`);
775
- }
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);
781
- }
782
- }
783
- debugLog('WARN', `${api.label} all retries failed`);
757
+ const maxSizeBytes = (config.maxImageSize ?? 0) * 1024 * 1024;
758
+ const contentLength = Number(response.headers['content-length'] || 0);
759
+ if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
760
+ writer.destroy();
761
+ await promises_1.default.unlink(filePath).catch(() => { });
762
+ throw new Error(`图片文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxImageSize}MB)`);
763
+ }
764
+ try {
765
+ await (0, promises_2.pipeline)(response.data, writer);
766
+ return filePath;
767
+ }
768
+ catch (e) {
769
+ await promises_1.default.unlink(filePath).catch(() => { });
770
+ throw new Error(`写入图片文件失败: ${getErrorMessage(e)}`);
784
771
  }
785
- throw lastError || new Error('所有API请求全部失败');
786
772
  }
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) {
773
+ async function sendImage(session, imageUrl) {
774
+ if (!config.showCoverImage)
775
+ return;
776
+ const sendLink = async () => { await sendWithTimeout(session, `图片链接:${imageUrl}`).catch(() => { }); };
777
+ if (config.forceDownloadImage) {
791
778
  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}`);
779
+ const localPath = await downloadImageFile(imageUrl);
780
+ await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
781
+ return;
796
782
  }
797
- catch (error) {
798
- debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
783
+ catch (e) {
784
+ debugLog('ERROR', '强制下载图片失败,尝试URL发送:', getErrorMessage(e));
785
+ try {
786
+ await sendWithTimeout(session, koishi_1.h.image(imageUrl));
787
+ return;
788
+ }
789
+ catch {
790
+ await sendLink();
791
+ }
799
792
  }
793
+ return;
800
794
  }
801
- return { success: false, msg: texts.unsupportedPlatformText };
802
- }
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++) {
795
+ if (!config.showImageFile) {
796
+ await sendLink();
797
+ return;
798
+ }
799
+ try {
800
+ await sendWithTimeout(session, koishi_1.h.image(imageUrl));
801
+ }
802
+ catch {
814
803
  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
- }
820
- else {
821
- return await sendPromise;
822
- }
804
+ const localPath = await downloadImageFile(imageUrl);
805
+ await sendWithTimeout(session, koishi_1.h.image(`file://${localPath}`));
823
806
  }
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;
807
+ catch {
808
+ await sendLink();
831
809
  }
832
810
  }
833
- return null;
834
811
  }
835
812
  async function sendVideoFile(session, videoUrl) {
836
813
  if (!videoUrl)
@@ -845,7 +822,7 @@ function apply(ctx, config) {
845
822
  return;
846
823
  }
847
824
  catch (e) {
848
- debugLog('ERROR', '强制下载失败,尝试URL发送:', getErrorMessage(e));
825
+ debugLog('ERROR', '强制下载视频失败,尝试URL发送:', getErrorMessage(e));
849
826
  try {
850
827
  await sendWithTimeout(session, koishi_1.h.video(videoUrl));
851
828
  return;
@@ -917,8 +894,9 @@ function apply(ctx, config) {
917
894
  const text = item.text;
918
895
  if (text && config.showImageText)
919
896
  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)))
897
+ if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
921
898
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
899
+ }
922
900
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
923
901
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
924
902
  for (const imgUrl of imageUrls)
@@ -948,21 +926,18 @@ function apply(ctx, config) {
948
926
  await sendWithTimeout(session, text);
949
927
  await delay(300);
950
928
  }
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(() => { });
929
+ if (p.cover && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
930
+ await sendImage(session, p.cover).catch(() => { });
953
931
  await delay(300);
954
932
  }
955
933
  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}`);
934
+ await sendVideoFile(session, p.video);
960
935
  await delay(500);
961
936
  }
962
937
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
963
938
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
964
939
  for (const imgUrl of imageUrls) {
965
- await sendWithTimeout(session, koishi_1.h.image(imgUrl)).catch(() => { });
940
+ await sendImage(session, imgUrl).catch(() => { });
966
941
  await delay(200);
967
942
  }
968
943
  }
@@ -970,6 +945,114 @@ function apply(ctx, config) {
970
945
  }
971
946
  debugLog('INFO', '处理完成');
972
947
  }
948
+ async function fetchApi(url, type, fieldMapping) {
949
+ const cacheKey = url;
950
+ const cached = urlCacheLocal.get(cacheKey);
951
+ if (cached && cached.expire > Date.now())
952
+ return cached.data;
953
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName } = getPlatformConfig(type);
954
+ const primaryApi = config.primaryApiUrl || 'https://api.bugpk.com/api/short_videos';
955
+ const backupApi = config.backupApiUrl || 'https://api.bugpk.com/api/svparse';
956
+ const backupAllowed = backupSupportedPlatforms.has(type);
957
+ const apiList = [];
958
+ if (dedicatedFirst && dedicatedUrl) {
959
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
960
+ apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
961
+ if (backupAllowed)
962
+ apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
963
+ }
964
+ else {
965
+ apiList.push({ url: primaryApi, label: '默认主API', fieldMapping });
966
+ if (backupAllowed)
967
+ apiList.push({ url: backupApi, label: '备用主API', fieldMapping });
968
+ if (dedicatedUrl)
969
+ apiList.push({ url: dedicatedUrl, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
970
+ }
971
+ const customHeaders = config.customHeaders || [];
972
+ let lastError = null;
973
+ for (const api of apiList) {
974
+ for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
975
+ try {
976
+ const headers = {
977
+ 'User-Agent': config.userAgent,
978
+ 'Referer': 'https://www.baidu.com/',
979
+ 'Content-Type': 'application/x-www-form-urlencoded'
980
+ };
981
+ for (const h of customHeaders) {
982
+ if (h.name && h.value)
983
+ headers[h.name] = h.value;
984
+ }
985
+ if (api.apiKey) {
986
+ const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
987
+ Object.assign(headers, authHeaders);
988
+ }
989
+ const res = await http.get(api.url, { params: { url }, timeout: config.timeout, headers });
990
+ if (res.data && (res.data.code === 200 || res.data.code === 0)) {
991
+ const parsed = parseApiResponse(res.data, config.maxDescLength, api.fieldMapping);
992
+ urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
993
+ return parsed;
994
+ }
995
+ throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
996
+ }
997
+ catch (error) {
998
+ lastError = error instanceof Error ? error : new Error(String(error));
999
+ debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
1000
+ if (attempt < config.retryTimes)
1001
+ await delay(config.retryInterval);
1002
+ }
1003
+ }
1004
+ debugLog('WARN', `${api.label} all retries failed`);
1005
+ }
1006
+ throw lastError || new Error('所有API请求全部失败');
1007
+ }
1008
+ async function parseUrl(url, type, fieldMapping) {
1009
+ const realUrl = await resolveShortUrl(url);
1010
+ const candidates = [...new Set([realUrl, url])];
1011
+ for (const candidate of candidates) {
1012
+ try {
1013
+ const info = await fetchApi(candidate, type, fieldMapping);
1014
+ if (info.video || info.images.length > 0 || info.live_photo.length > 0)
1015
+ return { success: true, data: info };
1016
+ debugLog('WARN', `解析成功但无内容: ${candidate}`);
1017
+ }
1018
+ catch (error) {
1019
+ debugLog('ERROR', `候选链接失败: ${candidate}`, getErrorMessage(error));
1020
+ }
1021
+ }
1022
+ return { success: false, msg: texts.unsupportedPlatformText };
1023
+ }
1024
+ async function processSingleUrl(url, type, fieldMapping) {
1025
+ const result = await parseUrl(url, type, fieldMapping);
1026
+ if (!result.success)
1027
+ return { success: false, msg: result.msg, url };
1028
+ const text = generateFormattedText(result.data, config.unifiedMessageFormat);
1029
+ return { success: true, data: { text, parsed: result.data } };
1030
+ }
1031
+ async function sendWithTimeout(session, content, customRetries) {
1032
+ const maxRetries = customRetries ?? config.retryTimes ?? 3;
1033
+ const retryDelay = config.retryInterval || 1000;
1034
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1035
+ try {
1036
+ let sendPromise = session.send(content);
1037
+ if (config.videoSendTimeout > 0) {
1038
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout));
1039
+ return await Promise.race([sendPromise, timeoutPromise]);
1040
+ }
1041
+ else {
1042
+ return await sendPromise;
1043
+ }
1044
+ }
1045
+ catch (err) {
1046
+ const errMsg = getErrorMessage(err);
1047
+ debugLog('ERROR', `发送失败尝试 ${attempt + 1}: ${errMsg}`);
1048
+ if (attempt < maxRetries)
1049
+ await delay(retryDelay);
1050
+ else if (!config.ignoreSendError)
1051
+ throw err;
1052
+ }
1053
+ }
1054
+ return null;
1055
+ }
973
1056
  ctx.on('message', async (session) => {
974
1057
  if (!config.enable)
975
1058
  return;
@@ -1013,22 +1096,20 @@ function apply(ctx, config) {
1013
1096
  });
1014
1097
  const tempCleanupInterval = setInterval(async () => {
1015
1098
  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++;
1099
+ const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1100
+ for (const dir of dirs) {
1101
+ const files = await promises_1.default.readdir(dir);
1102
+ const now = Date.now();
1103
+ for (const file of files) {
1104
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1105
+ const filePath = path_1.default.join(dir, file);
1106
+ const stats = await promises_1.default.stat(filePath);
1107
+ if (now - stats.mtimeMs > 3600000) {
1108
+ await promises_1.default.unlink(filePath).catch(() => { });
1109
+ }
1027
1110
  }
1028
1111
  }
1029
1112
  }
1030
- if (deleted)
1031
- debugLog('INFO', `清理了 ${deleted} 个过期临时视频文件`);
1032
1113
  }
1033
1114
  catch (e) {
1034
1115
  debugLog('WARN', '清理临时文件失败:', e);
@@ -1042,11 +1123,14 @@ function apply(ctx, config) {
1042
1123
  });
1043
1124
  process.on('beforeExit', async () => {
1044
1125
  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(() => { });
1126
+ const dirs = [config.tempDir || './temp_videos', config.imageTempDir || './temp_images'];
1127
+ for (const dir of dirs) {
1128
+ const files = await promises_1.default.readdir(dir);
1129
+ for (const file of files) {
1130
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) || (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i))) {
1131
+ await promises_1.default.unlink(path_1.default.join(dir, file)).catch(() => { });
1132
+ }
1133
+ }
1050
1134
  }
1051
1135
  }
1052
1136
  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.3",
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评论:${评论数}\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
+ | `showImageFile` | boolean | true | 封面/图片是否以文件形式发送(关闭则只发链接) |
42
+ | `forceDownloadImage` | boolean | false | 强制下载封面/图片后发送 |
43
+ | `imageDownloadTimeout` | number | 60000 | 图片下载超时(毫秒) |
44
+ | `imageTempDir` | string | `./temp_images` | 临时封面/图片存储目录 |
45
+ | `maxImageSize` | number | 0 | 最大下载图片大小(MB),0 不限制 |
46
+ | `showVideoFile` | boolean | true | 视频是否以文件形式发送(关闭则只发链接) |
47
+ | `forceDownloadVideo` | boolean | false | 强制下载视频后发送 |
43
48
  | `videoDownloadTimeout` | number | 120000 | 视频下载超时(毫秒) |
44
49
  | `tempDir` | string | `./temp_videos` | 临时视频存储目录 |
45
- | `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 为不限制大小 |
46
- | `forceDownloadVideo` | boolean | false | 强制下载视频后发送 |
47
- | `maxConcurrent` | number | 3 | 批量解析时最大并发数,避免同时下载过多 |
50
+ | `maxVideoSize` | number | 0 | 最大下载视频大小(MB),0 不限制 |
51
+ | `maxDescLength` | number | 200 | 简介最大长度(字符) |
52
+ | `maxConcurrent` | number | 3 | 批量解析最大并发数 |
48
53
 
49
54
  ### 网络与 API 设置 (Network & API Settings)
50
55
  | 配置项 | 类型 | 默认值 | 说明 |
51
56
  |--------|------|--------|------|
52
57
  | `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`(头值) |
58
+ | `videoSendTimeout` | number | 60000 | 消息发送超时时间(毫秒,0 为不限制) |
59
+ | `userAgent` | string | `Mozilla/5.0 ...` | User-Agent |
60
+ | `proxy` | object | `{ enabled: false, protocol: "http", host: "127.0.0.1", port: 7890, auth: { username: "", password: "" } }` | HTTP/HTTPS 代理。`enabled` 开关(默认关闭),`protocol` 下拉选择 `http` 或 `https` |
61
+ | `customHeaders` | array | [] | 自定义请求头,每项含 `name` `value` |
57
62
 
58
63
  ### API 选择与回退设置 (API Selection & Fallback)
59
64
  | 配置项 | 类型 | 默认值 | 说明 |
60
65
  |--------|------|--------|------|
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 响应格式,默认已包含常用路径示例 |
66
+ | `primaryApiUrl` | string | `https://api.bugpk.com/api/short_videos` | 主 API 地址 |
67
+ | `backupApiUrl` | string | `https://api.bugpk.com/api/svparse` | 备用主 API,仅支持部分平台 |
68
+ | `platformDedicatedFirst` | object | 各平台均为 `false` | 平台专属 API 优先开关,键:`bilibili` |
69
+ | `customApis` | array | [] | 自定义平台专属 API,含 `platform`, `apiUrl`, `apiKey`, `authHeaderType`, `customHeaderName`, `fieldMapping` |
70
+ | `globalFieldMapping` | string | 预设字段映射 JSON | 全局字段映射,支持点号路径 |
66
71
 
67
72
  ### 错误与重试设置 (Error & Retry Settings)
68
73
  | 配置项 | 类型 | 默认值 | 说明 |
69
74
  |--------|------|--------|------|
70
- | `ignoreSendError` | boolean | true | 是否忽略消息发送失败,避免插件崩溃 |
71
- | `retryTimes` | number | 3 | API 请求及消息发送失败时的重试次数 |
72
- | `retryInterval` | number | 1000 | API 请求及消息发送重试的间隔时间(毫秒) |
75
+ | `ignoreSendError` | boolean | true | 忽略发送失败 |
76
+ | `retryTimes` | number | 3 | 重试次数 |
77
+ | `retryInterval` | number | 1000 | 重试间隔(毫秒) |
73
78
 
74
79
  ### 发送方式设置 (Send Mode Settings)
75
80
  | 配置项 | 类型 | 默认值 | 说明 |
76
81
  |--------|------|--------|------|
77
- | `enableForward` | boolean | false | 是否启用合并转发(仅 OneBot 平台),启用后视频与图文将整合进同一条合并消息 |
82
+ | `enableForward` | boolean | false | 启用合并转发(仅 OneBot 平台) |
78
83
 
79
84
  ### 缓存与去重设置 (Cache & Deduplication Settings)
80
85
  | 配置项 | 类型 | 默认值 | 说明 |
81
86
  |--------|------|--------|------|
82
- | `deduplicationInterval` | number | 180 | 禁止重复解析时间间隔(秒),0 为不限制。同一个链接在间隔内不会重复解析。 |
83
- | `cacheTTL` | number | 600 | 解析结果缓存时间(秒),0 为不缓存。缓存可减少重复 API 请求。 |
87
+ | `deduplicationInterval` | number | 180 | 去重间隔(秒) |
88
+ | `cacheTTL` | number | 600 | 缓存时间(秒) |
84
89
 
85
90
  ### 界面文字设置 (UI Text Settings)
86
91
  | 配置项 | 类型 | 默认值 | 说明 |
87
92
  |--------|------|--------|------|
88
- | `waitingTipText` | string | 正在解析视频,请稍候... | 解析等待提示文字 |
89
- | `unsupportedPlatformText` | string | 不支持该平台链接 | 不支持的平台提示 |
90
- | `invalidLinkText` | string | 无效的视频链接 | 无效链接提示(parse 指令) |
91
- | `parseErrorPrefix` | string | ❌ 解析失败: | 解析失败消息前缀 |
92
- | `parseErrorItemFormat` | string | `【${url}】: ${msg}` | 每条解析失败的展示格式,可用 ${url}(链接)和 ${msg}(错误信息) |
93
+ | `waitingTipText` | string | 正在解析视频,请稍候... | 等待提示 |
94
+ | `unsupportedPlatformText` | string | 不支持该平台链接 | 不支持平台提示 |
95
+ | `invalidLinkText` | string | 无效的视频链接 | 无效链接提示 |
96
+ | `parseErrorPrefix` | string | ❌ 解析失败: | 错误前缀 |
97
+ | `parseErrorItemFormat` | string | `【${url}】: ${msg}` | 错误项格式 |
93
98
 
94
99
  ## 支持的变量 (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")时该行会自动隐藏。
100
+ 在 `unifiedMessageFormat` 中可使用以下变量,空行自动隐藏:
101
+
102
+ | 变量名 | 说明 |
103
+ |--------|------|
104
+ | `${标题}` | 视频/图集标题 |
105
+ | `${作者}` | 作者名称 |
106
+ | `${简介}` | 内容简介 |
107
+ | `${视频时长}` | 视频时长(时:分:秒) |
108
+ | `${点赞数}` | 点赞数量 |
109
+ | `${收藏数}` | 收藏数量 |
110
+ | `${转发数}` | 转发/分享数量 |
111
+ | `${播放数}` | 播放量 |
112
+ | `${评论数}` | 评论数量 |
113
+ | `${发布时间}` | 发布时间(格式化) |
114
+ | `${图片数量}` | 图集/实况图片数量 |
115
+ | `${作者ID}` | 作者唯一标识ID |
116
+ | `${视频链接}` | 视频原始链接 |
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,17 +151,17 @@ 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
 
153
158
  | 贡献者 (Contributor) | 贡献内容 (Contribution) |
154
159
  |----------------------|-------------------------|
155
- | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
156
- | ShiraiKuroko003 | 修复消息格式设置问题并且PR-1.2.5版本已修复 |
157
- | cyavb | 提交功能建议-给自定义API添加KEY认证-已修复 |
158
- | Keep785 | 提交Bug-无法正常关闭发送封面-已修复 |
159
- | dzt2008 + Apricityx | 提交Bug-会对非支持视频平台URL进行误解析-已修复 |
160
+ | Minecraft-1314 | 插件完整开发 |
161
+ | ShiraiKuroko003 | 修复消息格式问题 |
162
+ | cyavb | 自定义API KEY认证 |
163
+ | Keep785 | 无法关闭发送封面 |
164
+ | dzt2008 + Apricityx | 误解析修复 |
160
165
  | JH-Ahua | BugPk-Api 支持 |
161
166
  | shangxue | 灵感来源 |
162
167
 
@@ -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!