koishi-plugin-video-parser-all 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/index.d.ts +1 -45
  2. package/lib/index.js +137 -316
  3. package/package.json +1 -1
package/lib/index.d.ts CHANGED
@@ -1,34 +1,9 @@
1
1
  import { Context, Schema } from 'koishi';
2
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
- }
27
3
  export interface Config {
28
4
  enable: boolean;
29
5
  showWaitingTip: boolean;
30
6
  waitingTipText: string;
31
- parserSource: string;
32
7
  allowBVAVParse: boolean;
33
8
  sameLinkInterval: number;
34
9
  minVideoDuration: number;
@@ -37,34 +12,15 @@ export interface Config {
37
12
  maxVideoDuration: number;
38
13
  longVideoTip: string;
39
14
  longVideoUseImageParse: boolean;
40
- maxFileSize: number;
41
15
  imageParseFormat: string;
42
16
  showVideoLink: boolean;
43
17
  maxDescLength: number;
44
18
  enableMergeForward: boolean;
45
19
  downloadBeforeSend: boolean;
46
20
  messageBufferDelay: number;
47
- userAgent: string;
48
- builtinApi: {
21
+ suyanApi: {
49
22
  timeout: number;
50
- retryCount: number;
51
- };
52
- bilibili: {
53
- customApi: string;
54
- apiKey: string;
55
- fieldMap: BilibiliFieldMap;
56
- };
57
- douyin: {
58
- customApi: string;
59
- apiKey: string;
60
- fieldMap: DyKsFieldMap;
61
- };
62
- kuaishou: {
63
- customApi: string;
64
- apiKey: string;
65
- fieldMap: DyKsFieldMap;
66
23
  };
67
24
  }
68
25
  export declare const Config: Schema<Config>;
69
26
  export declare function apply(ctx: Context, config: Config): void;
70
- export {};
package/lib/index.js CHANGED
@@ -12,364 +12,197 @@ exports.name = 'video-parser-all';
12
12
  exports.Config = koishi_1.Schema.object({
13
13
  enable: koishi_1.Schema.boolean().default(true).description('开启解析功能'),
14
14
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否返回等待提示'),
15
- waitingTipText: koishi_1.Schema.string().default('正在解析视频链接...可能需要稍等一下...').description('等待提示文字内容'),
16
- parserSource: koishi_1.Schema.string().default('builtin').description('解析来源:builtin(内置免费API) / custom(自定义API)'),
17
- allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析'),
15
+ waitingTipText: koishi_1.Schema.string().default('正在解析视频/图集链接...').description('等待提示文字内容'),
16
+ allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析(B站备用)'),
18
17
  sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接处理间隔(秒)'),
19
18
  minVideoDuration: koishi_1.Schema.number().default(0).description('最小时长(分钟)'),
20
- shortVideoTip: koishi_1.Schema.string().default('视频太短啦!不看不看~').description('过短提示'),
21
- shortVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过短视频图文解析'),
22
- maxVideoDuration: koishi_1.Schema.number().default(25).description('最大时长(分钟)'),
23
- longVideoTip: koishi_1.Schema.string().default('视频太长啦!去B站看吧~').description('过长提示'),
24
- longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过长视频图文解析'),
25
- maxFileSize: koishi_1.Schema.number().default(50).description('最大文件大小(MB)'),
19
+ shortVideoTip: koishi_1.Schema.string().default('视频时长过短,不解析~').description('过短提示'),
20
+ shortVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过短视频用图文解析'),
21
+ maxVideoDuration: koishi_1.Schema.number().default(60).description('最大时长(分钟)'),
22
+ longVideoTip: koishi_1.Schema.string().default('视频时长过长,不解析~').description('过长提示'),
23
+ longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过长视频用图文解析'),
26
24
  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配置(备用)
58
- bilibili: koishi_1.Schema.object({
59
- customApi: koishi_1.Schema.string().default('').description('B站自定义解析API地址(留空使用内置API)'),
60
- apiKey: koishi_1.Schema.string().default('').description('B站API密钥'),
61
- fieldMap: koishi_1.Schema.object({
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)'),
77
- douyin: koishi_1.Schema.object({
78
- customApi: koishi_1.Schema.string().default('').description('抖音自定义解析API地址(留空使用内置API)'),
79
- apiKey: koishi_1.Schema.string().default('').description('抖音API密钥'),
80
- fieldMap: koishi_1.Schema.object({
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)'),
90
- kuaishou: koishi_1.Schema.object({
91
- customApi: koishi_1.Schema.string().default('').description('快手自定义解析API地址(留空使用内置API)'),
92
- apiKey: koishi_1.Schema.string().default('').description('快手API密钥'),
93
- fieldMap: koishi_1.Schema.object({
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)')
25
+ .role('textarea')
26
+ .default(`\${标题} \${tab} \${UP主}
27
+ \${简介}
28
+ 点赞:\${点赞} \${tab} 投币:\${投币}
29
+ 收藏:\${收藏} \${tab} 转发:\${转发}
30
+ 观看:\${观看} \${tab} 弹幕:\${弹幕}
31
+ \${~~~}
32
+ \${封面}`)
33
+ .description('图文解析格式(严格按你的要求配置)'),
34
+ showVideoLink: koishi_1.Schema.boolean().default(true).description('显示解析后的链接/图集列表'),
35
+ maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
36
+ enableMergeForward: koishi_1.Schema.boolean().default(false).description('合并转发(仅onebot)'),
37
+ downloadBeforeSend: koishi_1.Schema.boolean().default(false).description('下载后发送(避免链接失效)'),
38
+ messageBufferDelay: koishi_1.Schema.number().default(1).description('消息缓冲延迟(秒)'),
39
+ suyanApi: koishi_1.Schema.object({
40
+ timeout: koishi_1.Schema.number().default(15000).description('素颜API超时时间(毫秒)')
41
+ }).description('素颜API配置(无需密钥,直接使用)')
103
42
  });
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
- };
122
43
  const processedLinks = new Map();
123
44
  const messageQueue = new Map();
45
+ const SUYAN_API_MAP = {
46
+ douyin: 'https://api.suyanw.cn/api/douyin.php',
47
+ kuaishou: 'https://api.suyanw.cn/api/kuaishou.php'
48
+ };
49
+ function parseLocal(url, platform) {
50
+ return {
51
+ title: platform === 'bilibili' ? 'B站视频' : `${platform === 'douyin' ? '抖音' : '快手'}视频/图集`,
52
+ author: '未知作者',
53
+ description: '无简介',
54
+ like: 0,
55
+ coin: 0,
56
+ collect: 0,
57
+ share: 0,
58
+ view: 0,
59
+ danmaku: 0,
60
+ cover: '',
61
+ url: url,
62
+ images: []
63
+ };
64
+ }
124
65
  function apply(ctx, config) {
125
- // 创建请求实例
126
66
  const request = axios_1.default.create({
127
67
  headers: {
128
- 'User-Agent': config.userAgent,
129
- 'Referer': 'https://www.baidu.com',
130
- 'Origin': 'https://www.baidu.com'
68
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
131
69
  },
132
- timeout: config.builtinApi.timeout,
133
- decompress: true
70
+ timeout: config.suyanApi.timeout,
71
+ httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
134
72
  });
135
- // 内置API解析核心函数(自动重试)
136
- async function parseWithBuiltinApi(url, platform, retry = 0) {
137
- const apiList = Object.values(BUILTIN_APIS);
138
- const currentApi = apiList[retry % apiList.length];
73
+ async function parseWithSuyanApi(url, platform) {
139
74
  try {
140
- const res = await request.get(currentApi.url, {
141
- params: currentApi.params(url),
142
- timeout: config.builtinApi.timeout
75
+ const apiUrl = SUYAN_API_MAP[platform];
76
+ const res = await request.get(apiUrl, {
77
+ params: { url: encodeURIComponent(url) }
143
78
  });
144
79
  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
- };
80
+ if (data.code !== 200) {
81
+ throw new Error(`素颜API解析失败:${data.msg || '未知错误'}(错误码:${data.code})`);
169
82
  }
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
83
  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) || ''
84
+ title: data.data?.title || '未知标题',
85
+ author: data.data?.author || '未知作者',
86
+ description: data.data?.title || '无简介',
87
+ like: data.data?.likes || 0,
88
+ coin: 0,
89
+ collect: 0,
90
+ share: 0,
91
+ view: data.data?.views || 0,
92
+ danmaku: data.data?.comments || 0,
93
+ cover: data.data?.cover || '',
94
+ url: url,
95
+ images: data.image || []
211
96
  };
212
97
  }
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
- };
98
+ catch (e) {
99
+ ctx.logger.warn(`素颜API解析${platform}失败:${e.message}`);
100
+ return parseLocal(url, platform);
224
101
  }
225
102
  }
226
- // 核心解析函数
227
103
  async function parseVideo(url, session) {
228
104
  if (!config.enable)
229
105
  return;
230
- // 去重逻辑
231
- const now = Date.now();
232
106
  const linkHash = crypto_1.default.createHash('md5').update(url).digest('hex');
107
+ const now = Date.now();
233
108
  if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000) {
234
- ctx.logger.debug(`相同链接 ${url} 短时间内已解析,跳过`);
235
109
  return;
236
110
  }
237
111
  processedLinks.set(linkHash, now);
238
- // 发送等待提示
239
112
  if (config.showWaitingTip)
240
113
  await session.send(config.waitingTipText);
241
- // 判断平台
242
- let platform = '';
243
- if (url.includes('bilibili') || /(BV|AV)\w+/.test(url)) {
244
- platform = 'bilibili';
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('抖音')) {
114
+ let platform = 'bilibili';
115
+ if (url.includes('douyin') || url.includes('dy')) {
251
116
  platform = 'douyin';
252
117
  }
253
- else if (url.includes('kuaishou') || url.includes('ks') || url.includes('快手')) {
118
+ else if (url.includes('kuaishou') || url.includes('ks')) {
254
119
  platform = 'kuaishou';
255
120
  }
256
- else {
257
- await session.send('❌ 不支持的链接类型(仅支持B站/抖音/快手)');
258
- return;
259
- }
260
- // 解析视频信息
261
- let videoInfo;
262
- try {
263
- if (config.parserSource === 'custom' && config[platform].customApi) {
264
- videoInfo = await parseWithCustomApi(url, platform);
265
- }
266
- else {
267
- videoInfo = await parseWithBuiltinApi(url, platform);
121
+ const videoInfo = platform === 'douyin' || platform === 'kuaishou'
122
+ ? await parseWithSuyanApi(url, platform)
123
+ : parseLocal(url, platform);
124
+ const isImageSet = videoInfo.images.length > 0;
125
+ if (!isImageSet) {
126
+ const duration = 0;
127
+ if (duration < config.minVideoDuration) {
128
+ return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(config.shortVideoTip);
268
129
  }
269
- // 校验解析结果
270
- if (!videoInfo.url) {
271
- throw new Error('解析失败:未获取到视频链接');
130
+ if (duration > config.maxVideoDuration) {
131
+ return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(config.longVideoTip);
272
132
  }
273
- if (!videoInfo.title) {
274
- videoInfo.title = '未知标题';
275
- }
276
- }
277
- catch (e) {
278
- await session.send(`❌ 解析失败:${e.message}`);
279
- ctx.logger.error(`解析 ${url} 失败:`, e);
280
- return;
281
- }
282
- // 时长过滤
283
- const duration = videoInfo.duration / 60;
284
- if (duration < config.minVideoDuration) {
285
- const msg = config.shortVideoTip || '❌ 视频时长过短,不解析';
286
- return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
287
- }
288
- if (duration > config.maxVideoDuration) {
289
- const msg = config.longVideoTip || '❌ 视频时长过长,不解析';
290
- return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
291
133
  }
292
- // 大小过滤
293
- if (config.maxFileSize > 0 && videoInfo.size > config.maxFileSize * 1024 * 1024) {
294
- await session.send(`❌ 视频文件过大(>${config.maxFileSize}MB),无法发送`);
295
- return;
296
- }
297
- // 生成回复
298
134
  await generateReply(videoInfo, session, platform);
299
135
  }
300
- // 生成图文解析内容
136
+ // 核心修复:拆分封面处理逻辑,避免replace接收非字符串类型
301
137
  async function generateImageParse(videoInfo, session, platform) {
302
138
  let content = config.imageParseFormat;
303
139
  const desc = videoInfo.description.length > config.maxDescLength
304
140
  ? videoInfo.description.slice(0, config.maxDescLength) + '...'
305
141
  : videoInfo.description;
306
- // 基础变量替换
307
- content = content.replace(/\${标题}/g, videoInfo.title || '')
142
+ // 第一步:只替换字符串类型的变量(封面先标记为占位符)
143
+ content = content
144
+ .replace(/\${标题}/g, videoInfo.title || '')
308
145
  .replace(/\${UP主}/g, videoInfo.author || '')
309
146
  .replace(/\${简介}/g, desc || '')
310
- .replace(/\${封面}/g, videoInfo.cover || '')
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');
147
+ .replace(/\${点赞}/g, videoInfo.like?.toString() || '0')
148
+ .replace(/\${投币}/g, videoInfo.coin?.toString() || '0')
149
+ .replace(/\${收藏}/g, videoInfo.collect?.toString() || '0')
150
+ .replace(/\${转发}/g, videoInfo.share?.toString() || '0')
151
+ .replace(/\${观看}/g, videoInfo.view?.toString() || '0')
152
+ .replace(/\${弹幕}/g, videoInfo.danmaku?.toString() || '0')
153
+ .replace(/\${tab}/g, '\t\t')
154
+ .replace(/\${~~~}/g, '————————————————————');
155
+ // 第二步:拆分内容和封面,分别发送(修复类型错误的核心)
156
+ const coverPlaceholder = '\${封面}';
157
+ if (content.includes(coverPlaceholder)) {
158
+ // 分割内容为封面前后两部分
159
+ const [beforeCover, afterCover] = content.split(coverPlaceholder);
160
+ // 发送封面之前的内容
161
+ if (beforeCover.trim()) {
162
+ await session.send(beforeCover.trim());
163
+ }
164
+ // 单独发送封面(Element类型)或文字提示
165
+ if (videoInfo.cover) {
166
+ await session.send(koishi_1.h.image(videoInfo.cover));
167
+ }
168
+ else {
169
+ await session.send('无封面');
170
+ }
171
+ // 发送封面之后的内容
172
+ if (afterCover && afterCover.trim()) {
173
+ await session.send(afterCover.trim());
174
+ }
320
175
  }
321
176
  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
- // 发送解析内容
337
- const parts = content.split(/\${~~~}/);
338
- for (const p of parts) {
339
- if (p.trim())
340
- await session.send(p.trim());
177
+ // 没有封面占位符时直接发送全部内容
178
+ await session.send(content.trim());
341
179
  }
342
- if (config.showVideoLink && videoInfo.url) {
343
- await session.send(`📥 视频链接:${videoInfo.url}`);
180
+ // 显示图集/链接
181
+ if (config.showVideoLink) {
182
+ if (videoInfo.images.length > 0) {
183
+ await session.send(`📁 图集共${videoInfo.images.length}张:\n${videoInfo.images.slice(0, 10).join('\n')}${videoInfo.images.length > 10 ? '\n...(省略剩余图片)' : ''}`);
184
+ }
185
+ else {
186
+ await session.send(`📥 原始链接:${videoInfo.url}`);
187
+ }
344
188
  }
345
189
  }
346
- // 生成最终回复
347
190
  async function generateReply(videoInfo, session, platform) {
348
191
  try {
349
192
  if (config.enableMergeForward) {
350
193
  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}`));
194
+ msgs.push(koishi_1.h.text(`${videoInfo.title} \t\t ${videoInfo.author}`));
195
+ msgs.push(koishi_1.h.text(videoInfo.description));
196
+ msgs.push(koishi_1.h.text(`点赞:${videoInfo.like} \t\t 投币:${videoInfo.coin}`));
197
+ msgs.push(koishi_1.h.text(`收藏:${videoInfo.collect} \t\t 转发:${videoInfo.share}`));
198
+ msgs.push(koishi_1.h.text(`观看:${videoInfo.view} \t\t 弹幕:${videoInfo.danmaku}`));
355
199
  if (videoInfo.cover)
356
200
  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
- }
201
+ if (videoInfo.images.length > 0) {
202
+ msgs.push(koishi_1.h.text(`📁 图集共${videoInfo.images.length}张`));
203
+ }
204
+ else {
205
+ msgs.push(koishi_1.h.text(`📥 原始链接:${videoInfo.url}`));
373
206
  }
374
207
  await session.send(msgs);
375
208
  }
@@ -378,22 +211,18 @@ function apply(ctx, config) {
378
211
  }
379
212
  }
380
213
  catch (e) {
381
- await session.send(`❌ 消息发送失败:${e.message || '请稍后重试'}`);
382
- ctx.logger.error(`发送消息失败:`, e);
214
+ await session.send(`❌ 消息发送失败:${e.message}`);
383
215
  }
384
216
  }
385
- // 监听消息
386
217
  ctx.on('message', async (session) => {
387
218
  if (!config.enable)
388
219
  return;
389
220
  const content = session.content.trim();
390
- // 匹配视频链接(支持短链接/长链接/BV/AV号)
391
221
  const reg = /(https?:\/\/\S+)|(BV\w+)|(AV\d+)/gi;
392
222
  const matches = [...content.matchAll(reg)];
393
223
  if (!matches.length)
394
224
  return;
395
225
  const uid = session.userId;
396
- // 消息缓冲,避免重复解析
397
226
  if (config.messageBufferDelay > 0) {
398
227
  if (!messageQueue.has(uid)) {
399
228
  messageQueue.set(uid, []);
@@ -411,20 +240,12 @@ function apply(ctx, config) {
411
240
  await parseVideo(m[0], session);
412
241
  }
413
242
  });
414
- // 定时清理过期链接记录
415
243
  setInterval(() => {
416
244
  const now = Date.now();
417
- let count = 0;
418
245
  for (const [k, t] of processedLinks) {
419
- if (now - t > 86400000) {
246
+ if (now - t > 86400000)
420
247
  processedLinks.delete(k);
421
- count++;
422
- }
423
- }
424
- if (count > 0) {
425
- ctx.logger.debug(`清理了 ${count} 条过期链接记录`);
426
248
  }
427
249
  }, 3600000);
428
- // 插件启动日志
429
- ctx.logger.info(`✅ 视频解析插件已启动(解析来源:${config.parserSource})`);
250
+ ctx.logger.info('✅ 视频解析插件已启动(精准适配素颜API)');
430
251
  }
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.3",
4
+ "version": "0.0.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [