koishi-plugin-video-parser-all 0.0.1 → 0.0.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.
Files changed (3) hide show
  1. package/lib/index.d.ts +33 -48
  2. package/lib/index.js +312 -163
  3. package/package.json +1 -1
package/lib/index.d.ts CHANGED
@@ -1,11 +1,34 @@
1
1
  import { Context, Schema } from 'koishi';
2
- export declare const name = "video-parser";
2
+ export declare const name = "video-parser-all";
3
+ interface BilibiliFieldMap {
4
+ title: string;
5
+ author: string;
6
+ description: string;
7
+ like: string;
8
+ coin: string;
9
+ collect: string;
10
+ share: string;
11
+ view: string;
12
+ danmaku: string;
13
+ cover: string;
14
+ duration: string;
15
+ size: string;
16
+ url: string;
17
+ }
18
+ interface DyKsFieldMap {
19
+ title: string;
20
+ author: string;
21
+ description: string;
22
+ cover: string;
23
+ duration: string;
24
+ size: string;
25
+ url: string;
26
+ }
3
27
  export interface Config {
4
28
  enable: boolean;
5
29
  showWaitingTip: boolean;
6
30
  waitingTipText: string;
7
31
  parserSource: string;
8
- returnComponents: string[];
9
32
  allowBVAVParse: boolean;
10
33
  sameLinkInterval: number;
11
34
  minVideoDuration: number;
@@ -22,64 +45,26 @@ export interface Config {
22
45
  downloadBeforeSend: boolean;
23
46
  messageBufferDelay: number;
24
47
  userAgent: string;
25
- publicApiKey: string;
48
+ builtinApi: {
49
+ timeout: number;
50
+ retryCount: number;
51
+ };
26
52
  bilibili: {
27
53
  customApi: string;
28
54
  apiKey: string;
29
- fieldMap: {
30
- title: string;
31
- author: string;
32
- description: string;
33
- like: string;
34
- coin: string;
35
- collect: string;
36
- share: string;
37
- view: string;
38
- danmaku: string;
39
- cover: string;
40
- duration: string;
41
- size: string;
42
- url: string;
43
- };
55
+ fieldMap: BilibiliFieldMap;
44
56
  };
45
57
  douyin: {
46
58
  customApi: string;
47
59
  apiKey: string;
48
- fieldMap: {
49
- title: string;
50
- author: string;
51
- description: string;
52
- like: string;
53
- coin: string;
54
- collect: string;
55
- share: string;
56
- view: string;
57
- danmaku: string;
58
- cover: string;
59
- duration: string;
60
- size: string;
61
- url: string;
62
- };
60
+ fieldMap: DyKsFieldMap;
63
61
  };
64
62
  kuaishou: {
65
63
  customApi: string;
66
64
  apiKey: string;
67
- fieldMap: {
68
- title: string;
69
- author: string;
70
- description: string;
71
- like: string;
72
- coin: string;
73
- collect: string;
74
- share: string;
75
- view: string;
76
- danmaku: string;
77
- cover: string;
78
- duration: string;
79
- size: string;
80
- url: string;
81
- };
65
+ fieldMap: DyKsFieldMap;
82
66
  };
83
67
  }
84
68
  export declare const Config: Schema<Config>;
85
69
  export declare function apply(ctx: Context, config: Config): void;
70
+ export {};
package/lib/index.js CHANGED
@@ -8,14 +8,12 @@ exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const axios_1 = __importDefault(require("axios"));
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
- exports.name = 'video-parser';
12
- // 简化 Schema 定义,避免类型冲突
11
+ exports.name = 'video-parser-all';
13
12
  exports.Config = koishi_1.Schema.object({
14
13
  enable: koishi_1.Schema.boolean().default(true).description('开启解析功能'),
15
14
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否返回等待提示'),
16
15
  waitingTipText: koishi_1.Schema.string().default('正在解析视频链接...可能需要稍等一下...').description('等待提示文字内容'),
17
- parserSource: koishi_1.Schema.string().default('public').description('解析来源:public/自定义'),
18
- returnComponents: koishi_1.Schema.array(String).default(['title', 'author', 'cover']).description('返回内容组件'),
16
+ parserSource: koishi_1.Schema.string().default('builtin').description('解析来源:builtin(内置免费API) / custom(自定义API)'),
19
17
  allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析'),
20
18
  sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接处理间隔(秒)'),
21
19
  minVideoDuration: koishi_1.Schema.number().default(0).description('最小时长(分钟)'),
@@ -25,234 +23,377 @@ exports.Config = koishi_1.Schema.object({
25
23
  longVideoTip: koishi_1.Schema.string().default('视频太长啦!去B站看吧~').description('过长提示'),
26
24
  longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过长视频图文解析'),
27
25
  maxFileSize: koishi_1.Schema.number().default(50).description('最大文件大小(MB)'),
28
- imageParseFormat: koishi_1.Schema.string().default('${标题} ${tab} ${UP主}\n${简介}\n点赞:${点赞} ${tab} 投币:${投币}\n收藏:${收藏} ${tab} 转发:${转发}\n观看:${观看} ${tab} 弹幕:${弹幕}\n${~~~}\n${封面}').description('图文解析格式'),
29
- showVideoLink: koishi_1.Schema.boolean().default(false).description('显示视频链接'),
30
- maxDescLength: koishi_1.Schema.number().default(50).description('简介最大长度'),
31
- enableMergeForward: koishi_1.Schema.boolean().default(false).description('开启合并转发(仅onebot)'),
32
- downloadBeforeSend: koishi_1.Schema.boolean().default(false).description('下载后发送'),
33
- messageBufferDelay: koishi_1.Schema.number().default(0).description('消息缓冲延迟(秒)'),
34
- userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36').description('请求UA'),
35
- publicApiKey: koishi_1.Schema.string().default('').description('公共解析API密钥'),
26
+ imageParseFormat: koishi_1.Schema.string()
27
+ .default(`\
28
+ ${'='.repeat(40)}
29
+ 标题:${'${标题}'}
30
+ UP主:${'${UP主}'}
31
+ 简介:${'${简介}'}
32
+ ${'='.repeat(40)}
33
+ 点赞:${'${点赞}'} 投币:${'${投币}'}
34
+ 收藏:${'${收藏}'} 转发:${'${转发}'}
35
+ 观看:${'${观看}'} 弹幕:${'${弹幕}'}
36
+ ${'='.repeat(40)}
37
+ ${'${封面}'}
38
+ `)
39
+ .description(`图文解析的返回格式(支持换行)
40
+ 注意变量格式,以及变量名称:
41
+ - ${'${标题}'} / ${'${UP主}'} / ${'${简介}'}
42
+ - ${'${点赞}'} / ${'${投币}'} / ${'${收藏}'} / ${'${转发}'}
43
+ - ${'${观看}'} / ${'${弹幕}'} / ${'${封面}'}
44
+ - ${'${tab}'} 制表符 | ${'${~~~}'} 分割线
45
+ 抖音/快手会自动过滤点赞/投币等统计字段`),
46
+ showVideoLink: koishi_1.Schema.boolean().default(true).description('在末尾显示视频的链接地址(推荐开启)'),
47
+ maxDescLength: koishi_1.Schema.number().default(100).description('视频的简介最大的字符长度'),
48
+ enableMergeForward: koishi_1.Schema.boolean().default(false).description('是否开启合并转发 仅支持 onebot 适配器 其他平台开启 无效'),
49
+ downloadBeforeSend: koishi_1.Schema.boolean().default(false).description('是否将视频链接下载后再发送 (以解决部分onebot协议端的问题)'),
50
+ messageBufferDelay: koishi_1.Schema.number().default(1).description('消息接收缓冲延迟(秒),避免重复解析'),
51
+ userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36').description('所有 API 请求所用的 User-Agent'),
52
+ // 内置免费API配置
53
+ builtinApi: koishi_1.Schema.object({
54
+ timeout: koishi_1.Schema.number().default(10000).description('内置API请求超时时间(毫秒)'),
55
+ retryCount: koishi_1.Schema.number().default(2).description('内置API失败重试次数')
56
+ }).description('内置免费解析API配置'),
57
+ // 自定义API配置(备用)
36
58
  bilibili: koishi_1.Schema.object({
37
- customApi: koishi_1.Schema.string().default('').description('B站自定义API地址'),
59
+ customApi: koishi_1.Schema.string().default('').description('B站自定义解析API地址(留空使用内置API)'),
38
60
  apiKey: koishi_1.Schema.string().default('').description('B站API密钥'),
39
61
  fieldMap: koishi_1.Schema.object({
40
- title: koishi_1.Schema.string().default('title'),
41
- author: koishi_1.Schema.string().default('author'),
42
- description: koishi_1.Schema.string().default('desc'),
43
- like: koishi_1.Schema.string().default('likeCount'),
44
- coin: koishi_1.Schema.string().default('coinCount'),
45
- collect: koishi_1.Schema.string().default('collectCount'),
46
- share: koishi_1.Schema.string().default('shareCount'),
47
- view: koishi_1.Schema.string().default('viewCount'),
48
- danmaku: koishi_1.Schema.string().default('danmakuCount'),
49
- cover: koishi_1.Schema.string().default('cover'),
50
- duration: koishi_1.Schema.string().default('duration'),
51
- size: koishi_1.Schema.string().default('size'),
52
- url: koishi_1.Schema.string().default('video_url')
53
- }).description('B站字段映射')
54
- }).description('B站配置'),
62
+ title: koishi_1.Schema.string().default('title').description('标题字段映射'),
63
+ author: koishi_1.Schema.string().default('author').description('UP主字段映射'),
64
+ description: koishi_1.Schema.string().default('desc').description('简介字段映射'),
65
+ like: koishi_1.Schema.string().default('likeCount').description('点赞字段映射'),
66
+ coin: koishi_1.Schema.string().default('coinCount').description('投币字段映射'),
67
+ collect: koishi_1.Schema.string().default('collectCount').description('收藏字段映射'),
68
+ share: koishi_1.Schema.string().default('shareCount').description('转发字段映射'),
69
+ view: koishi_1.Schema.string().default('viewCount').description('观看字段映射'),
70
+ danmaku: koishi_1.Schema.string().default('danmakuCount').description('弹幕字段映射'),
71
+ cover: koishi_1.Schema.string().default('cover').description('封面字段映射'),
72
+ duration: koishi_1.Schema.string().default('duration').description('时长字段映射'),
73
+ size: koishi_1.Schema.string().default('size').description('文件大小字段映射'),
74
+ url: koishi_1.Schema.string().default('video_url').description('视频链接字段映射')
75
+ }).description('B站返回字段映射')
76
+ }).description('B站自定义API配置(留空使用内置免费API)'),
55
77
  douyin: koishi_1.Schema.object({
56
- customApi: koishi_1.Schema.string().default('').description('抖音自定义API地址'),
78
+ customApi: koishi_1.Schema.string().default('').description('抖音自定义解析API地址(留空使用内置API)'),
57
79
  apiKey: koishi_1.Schema.string().default('').description('抖音API密钥'),
58
80
  fieldMap: koishi_1.Schema.object({
59
- title: koishi_1.Schema.string().default('title'),
60
- author: koishi_1.Schema.string().default('author'),
61
- description: koishi_1.Schema.string().default('desc'),
62
- like: koishi_1.Schema.string().default('likeCount'),
63
- coin: koishi_1.Schema.string().default('coinCount'),
64
- collect: koishi_1.Schema.string().default('collectCount'),
65
- share: koishi_1.Schema.string().default('shareCount'),
66
- view: koishi_1.Schema.string().default('playCount'),
67
- danmaku: koishi_1.Schema.string().default('commentCount'),
68
- cover: koishi_1.Schema.string().default('cover'),
69
- duration: koishi_1.Schema.string().default('duration'),
70
- size: koishi_1.Schema.string().default('size'),
71
- url: koishi_1.Schema.string().default('video_url')
72
- }).description('抖音字段映射')
73
- }).description('抖音配置'),
81
+ title: koishi_1.Schema.string().default('title').description('标题字段映射'),
82
+ author: koishi_1.Schema.string().default('author').description('作者字段映射'),
83
+ description: koishi_1.Schema.string().default('desc').description('简介字段映射'),
84
+ cover: koishi_1.Schema.string().default('cover').description('封面字段映射'),
85
+ duration: koishi_1.Schema.string().default('duration').description('时长字段映射'),
86
+ size: koishi_1.Schema.string().default('size').description('文件大小字段映射'),
87
+ url: koishi_1.Schema.string().default('video_url').description('视频链接字段映射')
88
+ }).description('抖音返回字段映射')
89
+ }).description('抖音自定义API配置(留空使用内置免费API)'),
74
90
  kuaishou: koishi_1.Schema.object({
75
- customApi: koishi_1.Schema.string().default('').description('快手自定义API地址'),
91
+ customApi: koishi_1.Schema.string().default('').description('快手自定义解析API地址(留空使用内置API)'),
76
92
  apiKey: koishi_1.Schema.string().default('').description('快手API密钥'),
77
93
  fieldMap: koishi_1.Schema.object({
78
- title: koishi_1.Schema.string().default('title'),
79
- author: koishi_1.Schema.string().default('author'),
80
- description: koishi_1.Schema.string().default('desc'),
81
- like: koishi_1.Schema.string().default('likeCount'),
82
- coin: koishi_1.Schema.string().default('coinCount'),
83
- collect: koishi_1.Schema.string().default('collectCount'),
84
- share: koishi_1.Schema.string().default('shareCount'),
85
- view: koishi_1.Schema.string().default('playCount'),
86
- danmaku: koishi_1.Schema.string().default('commentCount'),
87
- cover: koishi_1.Schema.string().default('cover'),
88
- duration: koishi_1.Schema.string().default('duration'),
89
- size: koishi_1.Schema.string().default('size'),
90
- url: koishi_1.Schema.string().default('video_url')
91
- }).description('快手字段映射')
92
- }).description('快手配置')
94
+ title: koishi_1.Schema.string().default('title').description('标题字段映射'),
95
+ author: koishi_1.Schema.string().default('author').description('作者字段映射'),
96
+ description: koishi_1.Schema.string().default('desc').description('简介字段映射'),
97
+ cover: koishi_1.Schema.string().default('cover').description('封面字段映射'),
98
+ duration: koishi_1.Schema.string().default('duration').description('时长字段映射'),
99
+ size: koishi_1.Schema.string().default('size').description('文件大小字段映射'),
100
+ url: koishi_1.Schema.string().default('video_url').description('视频链接字段映射')
101
+ }).description('快手返回字段映射')
102
+ }).description('快手自定义API配置(留空使用内置免费API)')
93
103
  });
104
+ // 内置免费解析API列表(自动切换重试)
105
+ const BUILTIN_APIS = {
106
+ // API 1: 稳定免费的视频解析接口
107
+ api1: {
108
+ url: 'https://jx.jsonapi.cn/api.php',
109
+ params: (url) => ({ url, type: 'json' })
110
+ },
111
+ // API 2: 备用解析接口
112
+ api2: {
113
+ url: 'https://api.vvhan.com/api/video',
114
+ params: (url) => ({ url, type: 'json' })
115
+ },
116
+ // API 3: 兜底解析接口
117
+ api3: {
118
+ url: 'https://www.xiaoyangapi.com/api/video/analysis',
119
+ params: (url) => ({ url, appid: '1001', appkey: 'xiaoyangapi' })
120
+ }
121
+ };
94
122
  const processedLinks = new Map();
95
123
  const messageQueue = new Map();
96
124
  function apply(ctx, config) {
125
+ // 创建请求实例
97
126
  const request = axios_1.default.create({
98
- headers: { 'User-Agent': config.userAgent },
99
- timeout: 30000
127
+ headers: {
128
+ 'User-Agent': config.userAgent,
129
+ 'Referer': 'https://www.baidu.com',
130
+ 'Origin': 'https://www.baidu.com'
131
+ },
132
+ timeout: config.builtinApi.timeout,
133
+ decompress: true
100
134
  });
135
+ // 内置API解析核心函数(自动重试)
136
+ async function parseWithBuiltinApi(url, platform, retry = 0) {
137
+ const apiList = Object.values(BUILTIN_APIS);
138
+ const currentApi = apiList[retry % apiList.length];
139
+ try {
140
+ const res = await request.get(currentApi.url, {
141
+ params: currentApi.params(url),
142
+ timeout: config.builtinApi.timeout
143
+ });
144
+ const data = res.data;
145
+ if (!data || (data.code && data.code !== 200 && data.code !== 0)) {
146
+ throw new Error(`API返回错误:${data.msg || '未知错误'}`);
147
+ }
148
+ // 统一返回格式
149
+ const result = {
150
+ title: data.title || data.video_title || '',
151
+ author: data.author || data.nickname || data.up主 || '',
152
+ description: data.desc || data.description || data.content || '',
153
+ cover: data.cover || data.cover_url || data.thumbnail || '',
154
+ url: data.url || data.video_url || data.play || '',
155
+ duration: data.duration || data.time || 0,
156
+ size: data.size || 0
157
+ };
158
+ // B站额外补充统计字段
159
+ if (platform === 'bilibili') {
160
+ return {
161
+ ...result,
162
+ like: data.like || data.likeCount || 0,
163
+ coin: data.coin || data.coinCount || 0,
164
+ collect: data.collect || data.collectCount || 0,
165
+ share: data.share || data.shareCount || 0,
166
+ view: data.playCount || data.view || data.views || 0,
167
+ danmaku: data.danmaku || data.comment || 0
168
+ };
169
+ }
170
+ return result;
171
+ }
172
+ catch (e) {
173
+ // 重试逻辑
174
+ if (retry < config.builtinApi.retryCount) {
175
+ ctx.logger.warn(`内置API ${retry + 1} 解析失败:${e.message},重试下一个API...`);
176
+ return parseWithBuiltinApi(url, platform, retry + 1);
177
+ }
178
+ throw new Error(`所有内置API解析失败:${e.message}`);
179
+ }
180
+ }
181
+ // 自定义API解析函数
182
+ async function parseWithCustomApi(url, platform) {
183
+ const platformConfig = config[platform];
184
+ if (!platformConfig.customApi) {
185
+ throw new Error(`${platform} 自定义API地址未配置`);
186
+ }
187
+ const res = await request.get(platformConfig.customApi, {
188
+ params: { url, key: platformConfig.apiKey },
189
+ timeout: config.builtinApi.timeout
190
+ });
191
+ const data = res.data.data || res.data;
192
+ const getValue = (obj, path) => {
193
+ return path.split('.').reduce((o, k) => o?.[k], obj);
194
+ };
195
+ if (platform === 'bilibili') {
196
+ const fieldMap = platformConfig.fieldMap;
197
+ return {
198
+ title: getValue(data, fieldMap.title) || '',
199
+ author: getValue(data, fieldMap.author) || '',
200
+ description: getValue(data, fieldMap.description) || '',
201
+ like: getValue(data, fieldMap.like) || 0,
202
+ coin: getValue(data, fieldMap.coin) || 0,
203
+ collect: getValue(data, fieldMap.collect) || 0,
204
+ share: getValue(data, fieldMap.share) || 0,
205
+ view: getValue(data, fieldMap.view) || 0,
206
+ danmaku: getValue(data, fieldMap.danmaku) || 0,
207
+ cover: getValue(data, fieldMap.cover) || '',
208
+ duration: getValue(data, fieldMap.duration) || 0,
209
+ size: getValue(data, fieldMap.size) || 0,
210
+ url: getValue(data, fieldMap.url) || ''
211
+ };
212
+ }
213
+ else {
214
+ const fieldMap = platformConfig.fieldMap;
215
+ return {
216
+ title: getValue(data, fieldMap.title) || '',
217
+ author: getValue(data, fieldMap.author) || '',
218
+ description: getValue(data, fieldMap.description) || '',
219
+ cover: getValue(data, fieldMap.cover) || '',
220
+ duration: getValue(data, fieldMap.duration) || 0,
221
+ size: getValue(data, fieldMap.size) || 0,
222
+ url: getValue(data, fieldMap.url) || ''
223
+ };
224
+ }
225
+ }
226
+ // 核心解析函数
101
227
  async function parseVideo(url, session) {
102
228
  if (!config.enable)
103
229
  return;
230
+ // 去重逻辑
104
231
  const now = Date.now();
105
232
  const linkHash = crypto_1.default.createHash('md5').update(url).digest('hex');
106
- if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000)
233
+ if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000) {
234
+ ctx.logger.debug(`相同链接 ${url} 短时间内已解析,跳过`);
107
235
  return;
236
+ }
108
237
  processedLinks.set(linkHash, now);
238
+ // 发送等待提示
109
239
  if (config.showWaitingTip)
110
240
  await session.send(config.waitingTipText);
241
+ // 判断平台
111
242
  let platform = '';
112
- if (url.includes('bilibili') || /(BV|AV)\w+/.test(url))
243
+ if (url.includes('bilibili') || /(BV|AV)\w+/.test(url)) {
113
244
  platform = 'bilibili';
114
- else if (url.includes('douyin') || url.includes('dy'))
245
+ // BV/AV号补全链接
246
+ if (/^(BV|AV)/i.test(url)) {
247
+ url = `https://www.bilibili.com/video/${url}`;
248
+ }
249
+ }
250
+ else if (url.includes('douyin') || url.includes('dy') || url.includes('抖音')) {
115
251
  platform = 'douyin';
116
- else if (url.includes('kuaishou') || url.includes('ks'))
252
+ }
253
+ else if (url.includes('kuaishou') || url.includes('ks') || url.includes('快手')) {
117
254
  platform = 'kuaishou';
118
- else
119
- return await session.send('不支持的链接类型');
255
+ }
256
+ else {
257
+ await session.send('❌ 不支持的链接类型(仅支持B站/抖音/快手)');
258
+ return;
259
+ }
260
+ // 解析视频信息
120
261
  let videoInfo;
121
262
  try {
122
- if (config.parserSource === 'public') {
123
- videoInfo = await parsePublicApi(url, platform);
263
+ if (config.parserSource === 'custom' && config[platform].customApi) {
264
+ videoInfo = await parseWithCustomApi(url, platform);
124
265
  }
125
266
  else {
126
- videoInfo = await parseCustomApi(url, platform);
267
+ videoInfo = await parseWithBuiltinApi(url, platform);
268
+ }
269
+ // 校验解析结果
270
+ if (!videoInfo.url) {
271
+ throw new Error('解析失败:未获取到视频链接');
272
+ }
273
+ if (!videoInfo.title) {
274
+ videoInfo.title = '未知标题';
127
275
  }
128
276
  }
129
277
  catch (e) {
130
- await session.send('解析失败,请稍后重试');
278
+ await session.send(`❌ 解析失败:${e.message}`);
279
+ ctx.logger.error(`解析 ${url} 失败:`, e);
131
280
  return;
132
281
  }
282
+ // 时长过滤
133
283
  const duration = videoInfo.duration / 60;
134
284
  if (duration < config.minVideoDuration) {
135
- return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session) : await session.send(config.shortVideoTip);
285
+ const msg = config.shortVideoTip || '❌ 视频时长过短,不解析';
286
+ return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
136
287
  }
137
288
  if (duration > config.maxVideoDuration) {
138
- return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session) : await session.send(config.longVideoTip);
289
+ const msg = config.longVideoTip || '❌ 视频时长过长,不解析';
290
+ return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
139
291
  }
292
+ // 大小过滤
140
293
  if (config.maxFileSize > 0 && videoInfo.size > config.maxFileSize * 1024 * 1024) {
141
- return await session.send('视频文件过大,无法发送');
294
+ await session.send(`❌ 视频文件过大(>${config.maxFileSize}MB),无法发送`);
295
+ return;
142
296
  }
143
- await generateReply(videoInfo, session);
144
- }
145
- async function parsePublicApi(url, platform) {
146
- const res = await request.get('https://api.obtaindown.com/obApi/api/analysis', {
147
- params: { url, key: config.publicApiKey }
148
- });
149
- const data = res.data.data || {};
150
- return {
151
- title: data.title || '',
152
- author: data.author || data.userName || '',
153
- description: data.desc || data.description || '',
154
- like: data.likeCount || 0,
155
- coin: data.coinCount || 0,
156
- collect: data.collectCount || 0,
157
- share: data.shareCount || 0,
158
- view: data.viewCount || data.playCount || 0,
159
- danmaku: data.danmakuCount || data.commentCount || 0,
160
- cover: data.cover || data.thumbnail_url || '',
161
- duration: data.duration || 0,
162
- size: data.size || 0,
163
- url: data.video_url || data.download_url || ''
164
- };
165
- }
166
- async function parseCustomApi(url, platform) {
167
- const platformConfig = config[platform];
168
- const res = await request.get(platformConfig.customApi, {
169
- params: { url, key: platformConfig.apiKey }
170
- });
171
- const data = res.data.data || res.data;
172
- const fieldMap = platformConfig.fieldMap;
173
- // 简化嵌套字段取值逻辑
174
- const getValue = (obj, path) => {
175
- return path.split('.').reduce((o, k) => o?.[k], obj);
176
- };
177
- return {
178
- title: getValue(data, fieldMap.title) || '',
179
- author: getValue(data, fieldMap.author) || '',
180
- description: getValue(data, fieldMap.description) || '',
181
- like: getValue(data, fieldMap.like) || 0,
182
- coin: getValue(data, fieldMap.coin) || 0,
183
- collect: getValue(data, fieldMap.collect) || 0,
184
- share: getValue(data, fieldMap.share) || 0,
185
- view: getValue(data, fieldMap.view) || 0,
186
- danmaku: getValue(data, fieldMap.danmaku) || 0,
187
- cover: getValue(data, fieldMap.cover) || '',
188
- duration: getValue(data, fieldMap.duration) || 0,
189
- size: getValue(data, fieldMap.size) || 0,
190
- url: getValue(data, fieldMap.url) || ''
191
- };
297
+ // 生成回复
298
+ await generateReply(videoInfo, session, platform);
192
299
  }
193
- async function generateImageParse(videoInfo, session) {
300
+ // 生成图文解析内容
301
+ async function generateImageParse(videoInfo, session, platform) {
194
302
  let content = config.imageParseFormat;
195
303
  const desc = videoInfo.description.length > config.maxDescLength
196
304
  ? videoInfo.description.slice(0, config.maxDescLength) + '...'
197
305
  : videoInfo.description;
198
- content = content.replace(/\${标题}/g, videoInfo.title)
199
- .replace(/\${UP主}/g, videoInfo.author)
200
- .replace(/\${简介}/g, desc)
201
- .replace(/\${点赞}/g, videoInfo.like)
202
- .replace(/\${投币}/g, videoInfo.coin)
203
- .replace(/\${收藏}/g, videoInfo.collect)
204
- .replace(/\${转发}/g, videoInfo.share)
205
- .replace(/\${观看}/g, videoInfo.view)
206
- .replace(/\${弹幕}/g, videoInfo.danmaku)
207
- .replace(/\${封面}/g, videoInfo.cover)
306
+ // 基础变量替换
307
+ content = content.replace(/\${标题}/g, videoInfo.title || '')
308
+ .replace(/\${UP主}/g, videoInfo.author || '')
309
+ .replace(/\${简介}/g, desc || '')
310
+ .replace(/\${封面}/g, videoInfo.cover || '')
208
311
  .replace(/\${tab}/g, '\t');
312
+ // 仅B站替换统计字段
313
+ if (platform === 'bilibili') {
314
+ content = content.replace(/\${点赞}/g, videoInfo.like?.toString() || '0')
315
+ .replace(/\${投币}/g, videoInfo.coin?.toString() || '0')
316
+ .replace(/\${收藏}/g, videoInfo.collect?.toString() || '0')
317
+ .replace(/\${转发}/g, videoInfo.share?.toString() || '0')
318
+ .replace(/\${观看}/g, videoInfo.view?.toString() || '0')
319
+ .replace(/\${弹幕}/g, videoInfo.danmaku?.toString() || '0');
320
+ }
321
+ else {
322
+ // 抖音/快手清空统计字段
323
+ content = content.replace(/\${点赞}/g, '')
324
+ .replace(/\${投币}/g, '')
325
+ .replace(/\${收藏}/g, '')
326
+ .replace(/\${转发}/g, '')
327
+ .replace(/\${观看}/g, '')
328
+ .replace(/\${弹幕}/g, '');
329
+ // 清理空行和多余符号
330
+ content = content.replace(/点赞:\s*\t*\s*投币:\s*\n/g, '')
331
+ .replace(/收藏:\s*\t*\s*转发:\s*\n/g, '')
332
+ .replace(/观看:\s*\t*\s*弹幕:\s*\n/g, '')
333
+ .replace(/={2,}/g, (match) => match.trim() ? match : '')
334
+ .replace(/\n+/g, '\n').trim();
335
+ }
336
+ // 发送解析内容
209
337
  const parts = content.split(/\${~~~}/);
210
338
  for (const p of parts) {
211
339
  if (p.trim())
212
340
  await session.send(p.trim());
213
341
  }
214
- if (config.showVideoLink)
215
- await session.send(videoInfo.url);
342
+ if (config.showVideoLink && videoInfo.url) {
343
+ await session.send(`📥 视频链接:${videoInfo.url}`);
344
+ }
216
345
  }
217
- async function generateReply(videoInfo, session) {
218
- if (config.enableMergeForward) {
219
- // 简化合并转发逻辑,直接发送消息数组
220
- const msgs = [];
221
- if (config.returnComponents.includes('title'))
222
- msgs.push(koishi_1.h.text(videoInfo.title));
223
- if (config.returnComponents.includes('author'))
224
- msgs.push(koishi_1.h.text(videoInfo.author));
225
- if (config.returnComponents.includes('cover'))
226
- msgs.push(koishi_1.h.image(videoInfo.cover));
227
- if (config.downloadBeforeSend) {
228
- const stream = await request.get(videoInfo.url, { responseType: 'stream' });
229
- msgs.push(koishi_1.h.video(stream.data));
346
+ // 生成最终回复
347
+ async function generateReply(videoInfo, session, platform) {
348
+ try {
349
+ if (config.enableMergeForward) {
350
+ const msgs = [];
351
+ if (videoInfo.title)
352
+ msgs.push(koishi_1.h.text(`📌 标题:${videoInfo.title}`));
353
+ if (videoInfo.author)
354
+ msgs.push(koishi_1.h.text(`👤 作者:${videoInfo.author}`));
355
+ if (videoInfo.cover)
356
+ msgs.push(koishi_1.h.image(videoInfo.cover));
357
+ if (videoInfo.url) {
358
+ if (config.downloadBeforeSend) {
359
+ try {
360
+ const response = await request.get(videoInfo.url, {
361
+ responseType: 'stream',
362
+ timeout: 60000
363
+ });
364
+ msgs.push(koishi_1.h.video(response.data));
365
+ }
366
+ catch (e) {
367
+ msgs.push(koishi_1.h.text(`📥 视频链接:${videoInfo.url}`));
368
+ }
369
+ }
370
+ else {
371
+ msgs.push(koishi_1.h.text(`📥 视频链接:${videoInfo.url}`));
372
+ }
373
+ }
374
+ await session.send(msgs);
230
375
  }
231
376
  else {
232
- msgs.push(koishi_1.h.video(videoInfo.url));
377
+ await generateImageParse(videoInfo, session, platform);
233
378
  }
234
- await session.send(msgs);
235
379
  }
236
- else {
237
- await generateImageParse(videoInfo, session);
238
- if (config.downloadBeforeSend) {
239
- const stream = await request.get(videoInfo.url, { responseType: 'stream' });
240
- await session.send(koishi_1.h.video(stream.data));
241
- }
242
- else {
243
- await session.send(koishi_1.h.video(videoInfo.url));
244
- }
380
+ catch (e) {
381
+ await session.send(`❌ 消息发送失败:${e.message || '请稍后重试'}`);
382
+ ctx.logger.error(`发送消息失败:`, e);
245
383
  }
246
384
  }
385
+ // 监听消息
247
386
  ctx.on('message', async (session) => {
248
387
  if (!config.enable)
249
388
  return;
250
389
  const content = session.content.trim();
390
+ // 匹配视频链接(支持短链接/长链接/BV/AV号)
251
391
  const reg = /(https?:\/\/\S+)|(BV\w+)|(AV\d+)/gi;
252
392
  const matches = [...content.matchAll(reg)];
253
393
  if (!matches.length)
254
394
  return;
255
395
  const uid = session.userId;
396
+ // 消息缓冲,避免重复解析
256
397
  if (config.messageBufferDelay > 0) {
257
398
  if (!messageQueue.has(uid)) {
258
399
  messageQueue.set(uid, []);
@@ -270,12 +411,20 @@ function apply(ctx, config) {
270
411
  await parseVideo(m[0], session);
271
412
  }
272
413
  });
273
- // 清理过期链接记录
414
+ // 定时清理过期链接记录
274
415
  setInterval(() => {
275
416
  const now = Date.now();
417
+ let count = 0;
276
418
  for (const [k, t] of processedLinks) {
277
- if (now - t > 86400000)
419
+ if (now - t > 86400000) {
278
420
  processedLinks.delete(k);
421
+ count++;
422
+ }
423
+ }
424
+ if (count > 0) {
425
+ ctx.logger.debug(`清理了 ${count} 条过期链接记录`);
279
426
  }
280
427
  }, 3600000);
428
+ // 插件启动日志
429
+ ctx.logger.info(`✅ 视频解析插件已启动(解析来源:${config.parserSource})`);
281
430
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 视频解析插件,支持抖音/快手/B站链接解析,可自定义API和解析规则",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [