koishi-plugin-video-parser-all 0.1.9 → 0.2.1

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 +4 -16
  2. package/lib/index.js +166 -223
  3. package/package.json +1 -1
package/lib/index.d.ts CHANGED
@@ -18,22 +18,10 @@ export interface Config {
18
18
  enableForward: boolean;
19
19
  downloadVideoBeforeSend: boolean;
20
20
  messageBufferDelay: number;
21
- commonApi: string;
22
- douyin: {
23
- mode: 'common' | 'own' | 'custom';
24
- ownApi: string;
25
- customApi: string;
26
- };
27
- kuaishou: {
28
- mode: 'common' | 'own' | 'custom';
29
- ownApi: string;
30
- customApi: string;
31
- };
32
- bilibili: {
33
- mode: 'common' | 'own' | 'custom';
34
- ownApi: string;
35
- customApi: string;
36
- };
21
+ retryTimes: number;
22
+ retryInterval: number;
23
+ apiUrl: string;
24
+ videoSendTimeout: number;
37
25
  }
38
26
  export declare const Config: Schema<Config>;
39
27
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -16,57 +16,46 @@ exports.name = 'video-parser-all';
16
16
  exports.Config = koishi_1.Schema.object({
17
17
  enable: koishi_1.Schema.boolean().default(true).description('是否启用插件'),
18
18
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否显示解析等待提示'),
19
- revokeWaitingTip: koishi_1.Schema.boolean().default(true).description('是否撤回等待提示文本'),
20
- waitingTipText: koishi_1.Schema.string().default('正在解析视频…').description('等待提示文本'),
21
- sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接重复间隔秒'),
22
- imageParseFormat: koishi_1.Schema.string()
23
- .role('textarea')
24
- .default('${标题} ${tab} ${UP主}\n${简介}\n${~~~}\n${封面}')
25
- .description('图文格式:${标题} ${UP主} ${简介} ${点赞} ${投币} ${收藏} ${转发} ${观看} ${弹幕} ${tab} ${~~~} ${封面}'),
19
+ revokeWaitingTip: koishi_1.Schema.boolean().default(true).description('解析完成后是否撤回等待提示'),
20
+ waitingTipText: koishi_1.Schema.string().default('正在解析视频…').description('等待提示的文本内容'),
21
+ sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接解析间隔(秒),防止重复解析'),
22
+ imageParseFormat: koishi_1.Schema.string().role('textarea').default('${标题} ${tab} ${UP主}\n${简介}\n${~~~}\n${封面}').description(`解析结果文本格式,支持的变量:
23
+ - \${标题}:视频标题
24
+ - \${UP主}:作者名称
25
+ - \${简介}:视频简介
26
+ - \${点赞}:点赞数
27
+ - \${投币}:投币数
28
+ - \${收藏}:收藏数
29
+ - \${转发}:转发数
30
+ - \${观看}:播放量
31
+ - \${弹幕}:弹幕数
32
+ - \${tab}:制表符
33
+ - \${~~~}:换行符
34
+ - \${封面}:封面图片位置`),
26
35
  returnContent: koishi_1.Schema.object({
27
- showImageText: koishi_1.Schema.boolean().default(true).description('返回图文'),
28
- showVideoUrl: koishi_1.Schema.boolean().default(false).description('返回视频直链'),
29
- showVideoFile: koishi_1.Schema.boolean().default(true).description('返回视频')
30
- }).description('内容组件'),
31
- maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
32
- timeout: koishi_1.Schema.number().default(15000).description('请求超时毫秒'),
33
- ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送错误'),
34
- enableForward: koishi_1.Schema.boolean().default(false).description('合并转发(仅onebot)'),
35
- downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('先下载视频再发送(解决onebot问题)'),
36
- messageBufferDelay: koishi_1.Schema.number().default(1).min(0).description('消息缓冲延迟秒(可设为0)'),
37
- commonApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('通用解析API'),
38
- douyin: koishi_1.Schema.object({
39
- mode: koishi_1.Schema.union([
40
- koishi_1.Schema.const('common').description('使用通用API'),
41
- koishi_1.Schema.const('own').description('使用平台专属API'),
42
- koishi_1.Schema.const('custom').description('使用自定义API'),
43
- ]).default('common').description('抖音解析模式'),
44
- ownApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/douyin').description('抖音专属API'),
45
- customApi: koishi_1.Schema.string().description('抖音自定义API'),
46
- }).description('抖音配置'),
47
- kuaishou: koishi_1.Schema.object({
48
- mode: koishi_1.Schema.union([
49
- koishi_1.Schema.const('common').description('使用通用API'),
50
- koishi_1.Schema.const('own').description('使用平台专属API'),
51
- koishi_1.Schema.const('custom').description('使用自定义API'),
52
- ]).default('common').description('快手解析模式'),
53
- ownApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/ksjx').description('快手专属API'),
54
- customApi: koishi_1.Schema.string().description('快手自定义API'),
55
- }).description('快手配置'),
56
- bilibili: koishi_1.Schema.object({
57
- mode: koishi_1.Schema.union([
58
- koishi_1.Schema.const('common').description('使用通用API'),
59
- koishi_1.Schema.const('own').description('使用平台专属API'),
60
- koishi_1.Schema.const('custom').description('使用自定义API'),
61
- ]).default('common').description('B站解析模式'),
62
- ownApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/bilibili').description('B站专属API'),
63
- customApi: koishi_1.Schema.string().description('B站自定义API'),
64
- }).description('B站配置'),
36
+ showImageText: koishi_1.Schema.boolean().default(true).description('是否显示解析后的文本和封面'),
37
+ showVideoUrl: koishi_1.Schema.boolean().default(false).description('是否显示无水印视频链接'),
38
+ showVideoFile: koishi_1.Schema.boolean().default(true).description('是否发送视频文件/视频链接'),
39
+ }).description('返回内容配置'),
40
+ maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度限制'),
41
+ timeout: koishi_1.Schema.number().default(15000).description('API 请求超时时间(毫秒)'),
42
+ ignoreSendError: koishi_1.Schema.boolean().default(true).description('是否忽略发送消息时的错误'),
43
+ enableForward: koishi_1.Schema.boolean().default(false).description('是否启用合并转发模式(仅 OneBot 平台)'),
44
+ downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前是否先下载视频(仅 OneBot 平台,解决部分视频无法播放问题)'),
45
+ messageBufferDelay: koishi_1.Schema.number().default(1).min(0).description('消息缓冲延迟(秒),用于合并短时间内的多个解析请求'),
46
+ retryTimes: koishi_1.Schema.number().default(3).min(0).description('API 请求失败重试次数'),
47
+ retryInterval: koishi_1.Schema.number().default(2000).min(500).description('重试间隔(毫秒)'),
48
+ apiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('视频解析接口地址'),
49
+ videoSendTimeout: koishi_1.Schema.number().default(30000).description('视频消息发送超时时间(毫秒),解决发送视频超时问题')
65
50
  });
66
51
  if (!worker_threads_1.isMainThread) {
67
52
  const { url, filePath } = worker_threads_1.workerData;
68
53
  (async () => {
69
54
  try {
55
+ if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
56
+ worker_threads_1.parentPort?.postMessage({ success: false, error: '不支持音频' });
57
+ return;
58
+ }
70
59
  const response = await (0, axios_1.default)({
71
60
  url,
72
61
  method: 'GET',
@@ -80,10 +69,7 @@ if (!worker_threads_1.isMainThread) {
80
69
  worker_threads_1.parentPort?.postMessage({ success: true, filePath });
81
70
  }
82
71
  catch (error) {
83
- worker_threads_1.parentPort?.postMessage({
84
- success: false,
85
- error: error.message
86
- });
72
+ worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
87
73
  }
88
74
  })();
89
75
  }
@@ -91,8 +77,8 @@ const processed = new Map();
91
77
  const linkBuffer = new Map();
92
78
  const PLATFORM_KEYWORDS = {
93
79
  bilibili: ['bilibili', 'b23', 'B站'],
94
- kuaishou: ['kuaishou', '快手'],
95
- douyin: ['douyin', '抖音']
80
+ kuaishou: ['kuaishou', '快手', 'v.kuaishou.com'],
81
+ douyin: ['douyin', '抖音', 'v.douyin.com']
96
82
  };
97
83
  function extractUrl(content) {
98
84
  const urlMatches = content.match(/https?:\/\/[^\s]+/gi) || [];
@@ -121,30 +107,23 @@ async function downloadVideoWithThreads(url, filename) {
121
107
  if (!fs_1.default.existsSync(dir))
122
108
  fs_1.default.mkdirSync(dir, { recursive: true });
123
109
  const filePath = path_1.default.join(dir, `${filename}.mp4`);
124
- const worker = new worker_threads_1.Worker(__filename, {
125
- workerData: { url, filePath }
126
- });
110
+ const worker = new worker_threads_1.Worker(__filename, { workerData: { url, filePath } });
127
111
  worker.on('message', (result) => {
128
- if (result.success) {
112
+ if (result.success)
129
113
  resolve(result.filePath);
130
- }
131
- else {
114
+ else
132
115
  reject(new Error(result.error));
133
- }
134
- });
135
- worker.on('error', (error) => {
136
- reject(error);
137
116
  });
117
+ worker.on('error', reject);
138
118
  worker.on('exit', (code) => {
139
- if (code !== 0) {
140
- reject(new Error(`下载线程退出,代码: ${code}`));
141
- }
119
+ if (code !== 0)
120
+ reject(new Error('worker exit'));
142
121
  });
143
122
  });
144
123
  }
145
- function parseData(data, platform, maxDescLength) {
146
- let title = data.title || '无标题';
147
- let author = data.auther?.name || data.user?.name || data.auther || data.author || '未知作者';
124
+ function parseData(data, maxDescLength) {
125
+ let title = data.title || data.desc || '无标题';
126
+ let author = data.author || data.auther || data.user?.name || '未知作者';
148
127
  let desc = data.desc || data.description || title || '无简介';
149
128
  desc = desc.slice(0, maxDescLength);
150
129
  let digg = data.like || data.digg || 0;
@@ -155,34 +134,34 @@ function parseData(data, platform, maxDescLength) {
155
134
  let danmaku = data.danmaku || data.comment || 0;
156
135
  let cover = data.cover || data.imgurl || data.pic || '';
157
136
  let video = '';
158
- if (data.url)
137
+ if (data.videos && Array.isArray(data.videos) && data.videos.length > 0 && data.videos[0].url) {
138
+ video = data.videos[0].url;
139
+ }
140
+ else if (data.url) {
159
141
  video = data.url;
160
- else if (data.video_url)
161
- video = data.video_url;
162
- else if (data.videos && Array.isArray(data.videos) && data.videos.length > 0) {
163
- video = data.videos[0].url || '';
164
142
  }
165
- else if (data.play)
166
- video = data.play;
167
- else if (data.hd_url)
168
- video = data.hd_url;
143
+ else if (data.video_backup && Array.isArray(data.video_backup) && data.video_backup[0]?.url) {
144
+ video = data.video_backup[0].url;
145
+ }
146
+ if (video.endsWith('.m4a') || video.endsWith('.mp3'))
147
+ video = '';
169
148
  return { title, author, desc, digg, coin, collect, share, play, danmaku, cover, video };
170
149
  }
171
150
  function clearAllCache() {
172
151
  processed.clear();
173
- linkBuffer.forEach(buffer => clearTimeout(buffer.timer));
152
+ linkBuffer.forEach(b => clearTimeout(b.timer));
174
153
  linkBuffer.clear();
175
154
  const tempDir = path_1.default.join(process.cwd(), 'temp_videos');
176
155
  if (fs_1.default.existsSync(tempDir)) {
177
- fs_1.default.readdirSync(tempDir).forEach(file => {
156
+ fs_1.default.readdirSync(tempDir).forEach(f => {
178
157
  try {
179
- const filePath = path_1.default.join(tempDir, file);
180
- fs_1.default.unlinkSync(filePath);
158
+ fs_1.default.unlinkSync(path_1.default.join(tempDir, f));
181
159
  }
182
- catch (e) { }
160
+ catch { }
183
161
  });
184
162
  }
185
163
  }
164
+ const delay = (ms) => new Promise(r => setTimeout(r, ms));
186
165
  function apply(ctx, config) {
187
166
  if (!worker_threads_1.isMainThread)
188
167
  return;
@@ -193,47 +172,32 @@ function apply(ctx, config) {
193
172
  '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'
194
173
  }
195
174
  });
196
- function getApi(platform) {
197
- const conf = config[platform];
198
- if (conf.mode === 'custom' && conf.customApi)
199
- return conf.customApi;
200
- if (conf.mode === 'own')
201
- return conf.ownApi;
202
- return config.commonApi;
203
- }
204
175
  async function parse(url) {
205
176
  const platform = getPlatformType(url);
206
177
  if (!platform)
207
- return { data: null, platform: null };
208
- const api = getApi(platform);
209
- if (!api)
210
- return { data: null, platform: null };
211
- try {
212
- const res = await http.get(api, { params: { url } });
213
- if ((res.data.code === 200 || res.data.code === 0) && res.data.data) {
214
- return {
215
- data: parseData(res.data.data, platform, config.maxDescLength),
216
- platform
217
- };
218
- }
219
- else {
220
- ctx.logger.error(`API返回非成功状态: ${JSON.stringify(res.data)}`);
178
+ return { data: null };
179
+ for (let i = 0; i <= config.retryTimes; i++) {
180
+ try {
181
+ const res = await http.get(config.apiUrl, { params: { url } });
182
+ const isSuccess = (res.data.code === 200 || res.data.code === 0) && res.data.data;
183
+ if (isSuccess) {
184
+ return { data: parseData(res.data.data, config.maxDescLength) };
185
+ }
221
186
  }
187
+ catch (e) { }
188
+ if (i < config.retryTimes)
189
+ await delay(config.retryInterval);
222
190
  }
223
- catch (e) {
224
- ctx.logger.error(`解析失败: ${e.message}`);
225
- }
226
- return { data: null, platform: null };
191
+ return { data: null };
227
192
  }
228
193
  async function processSingleUrl(session, url) {
229
194
  const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
230
195
  const now = Date.now();
231
- if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
196
+ if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000)
232
197
  return null;
233
- }
234
198
  processed.set(hash, now);
235
199
  const parseResult = await parse(url);
236
- if (!parseResult.data)
200
+ if (!parseResult.data || !parseResult.data.video)
237
201
  return null;
238
202
  let text = config.imageParseFormat
239
203
  .replace(/\${标题}/g, parseResult.data.title)
@@ -245,28 +209,26 @@ function apply(ctx, config) {
245
209
  .replace(/\${转发}/g, String(parseResult.data.share))
246
210
  .replace(/\${观看}/g, String(parseResult.data.play))
247
211
  .replace(/\${弹幕}/g, String(parseResult.data.danmaku))
248
- .replace(/\${tab}/g, '\t')
249
- .replace(/\${~~~}/g, '\n');
212
+ .replace(/\${tab}/g, '\t').replace(/\${~~~}/g, '\n');
250
213
  const contentParts = [];
251
214
  if (config.returnContent.showImageText) {
252
- const [beforeCover, afterCover] = text.split('${封面}');
253
- if (beforeCover && beforeCover.trim())
254
- contentParts.push(beforeCover.trim());
215
+ const [b, a] = text.split('${封面}');
216
+ if (b?.trim())
217
+ contentParts.push(b.trim());
255
218
  if (parseResult.data.cover)
256
219
  contentParts.push(koishi_1.h.image(parseResult.data.cover));
257
- if (afterCover && afterCover.trim())
258
- contentParts.push(afterCover.trim());
220
+ if (a?.trim())
221
+ contentParts.push(a.trim());
259
222
  }
260
223
  let videoContent = '';
261
224
  if (config.returnContent.showVideoFile && parseResult.data.video) {
262
225
  if (config.downloadVideoBeforeSend && session.platform === 'onebot') {
263
226
  try {
264
- const filename = crypto_1.default.createHash('md5').update(parseResult.data.video).digest('hex');
265
- const filePath = await downloadVideoWithThreads(parseResult.data.video, filename);
266
- videoContent = koishi_1.h.file(filePath);
227
+ const name = crypto_1.default.createHash('md5').update(parseResult.data.video).digest('hex');
228
+ const fp = await downloadVideoWithThreads(parseResult.data.video, name);
229
+ videoContent = koishi_1.h.file(fp);
267
230
  }
268
231
  catch (e) {
269
- ctx.logger.error(`下载视频失败: ${e.message}`);
270
232
  videoContent = koishi_1.h.video(parseResult.data.video);
271
233
  }
272
234
  }
@@ -277,145 +239,126 @@ function apply(ctx, config) {
277
239
  if (config.returnContent.showVideoUrl && parseResult.data.video) {
278
240
  contentParts.push(`🔗 无水印链接:${parseResult.data.video}`);
279
241
  }
280
- return {
281
- textContent: contentParts.join('\n'),
282
- videoContent,
283
- rawData: parseResult.data
284
- };
242
+ return { textContent: contentParts.join('\n'), videoContent };
285
243
  }
286
- async function revokeWaitingTip(session, sessionKey) {
244
+ async function revokeTip(session, key) {
287
245
  if (!config.revokeWaitingTip || session.platform !== 'onebot')
288
246
  return;
289
- const bufferData = linkBuffer.get(sessionKey);
290
- if (!bufferData?.tipMsgId)
247
+ const buf = linkBuffer.get(key);
248
+ if (!buf?.tipMsgId)
291
249
  return;
292
250
  try {
293
- await session.bot.deleteMessage(session.channelId, bufferData.tipMsgId);
251
+ await session.bot.deleteMessage(session.channelId, buf.tipMsgId.toString());
294
252
  }
295
253
  catch (e) {
296
- ctx.logger.debug(`撤回等待提示失败: ${e.message}`);
254
+ ctx.logger.warn(`撤回提示消息失败: ${e.message}`);
297
255
  }
298
256
  }
299
- async function flushBuffer(session) {
300
- const sessionKey = `${session.platform}:${session.userId}:${session.channelId}`;
301
- const bufferData = linkBuffer.get(sessionKey);
302
- if (!bufferData)
257
+ async function sendWithTimeout(session, content, timeout) {
258
+ return Promise.race([
259
+ session.send(content),
260
+ new Promise((_, reject) => setTimeout(() => reject(new Error('消息发送超时')), timeout))
261
+ ]).catch((e) => {
262
+ ctx.logger.warn(`消息发送超时: ${e.message}`);
263
+ return null;
264
+ });
265
+ }
266
+ async function flush(session) {
267
+ const key = `${session.platform}:${session.userId}:${session.channelId}`;
268
+ const buf = linkBuffer.get(key);
269
+ if (!buf)
303
270
  return;
304
- clearTimeout(bufferData.timer);
305
- linkBuffer.delete(sessionKey);
306
- await revokeWaitingTip(session, sessionKey);
271
+ clearTimeout(buf.timer);
272
+ linkBuffer.delete(key);
273
+ await revokeTip(session, key);
307
274
  const results = [];
308
- for (const url of bufferData.urls) {
309
- const result = await processSingleUrl(session, url);
310
- if (result)
311
- results.push(result);
275
+ for (const u of buf.urls) {
276
+ const r = await processSingleUrl(session, u);
277
+ if (r)
278
+ results.push(r);
312
279
  }
313
280
  if (results.length === 0) {
314
- await session.send('视频解析失败,请稍后重试');
281
+ await sendWithTimeout(session, '视频解析失败', config.videoSendTimeout);
315
282
  return;
316
283
  }
317
284
  if (config.enableForward && session.platform === 'onebot') {
318
285
  try {
319
- const forwardMsg = (0, koishi_1.h)('message', {
320
- type: 'forward',
321
- data: {
322
- messages: results.map(result => {
323
- const content = [];
324
- if (result.textContent)
325
- content.push(result.textContent);
326
- if (result.videoContent)
327
- content.push(result.videoContent);
328
- return {
329
- type: 'node',
330
- data: {
331
- name: '视频解析机器人',
332
- uin: session.selfId,
333
- content: content
334
- }
335
- };
336
- })
337
- }
286
+ const forwardMessages = results.map(result => {
287
+ const content = [];
288
+ if (result.textContent)
289
+ content.push(result.textContent);
290
+ if (result.videoContent)
291
+ content.push(result.videoContent);
292
+ return (0, koishi_1.h)('message', [
293
+ (0, koishi_1.h)('author', { id: session.selfId, name: '解析机器人' }),
294
+ ...content
295
+ ]);
338
296
  });
339
- await session.send(forwardMsg);
297
+ await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages), config.videoSendTimeout);
340
298
  return;
341
299
  }
342
300
  catch (e) {
343
- ctx.logger.error(`合并转发失败: ${e.message}`);
344
- await session.send('合并转发失败,将分开发送');
301
+ await sendWithTimeout(session, '合并转发失败,将分开发送', config.videoSendTimeout);
345
302
  }
346
303
  }
347
- for (const result of results) {
304
+ for (const r of results) {
348
305
  try {
349
- if (result.textContent)
350
- await session.send(result.textContent);
351
- if (result.videoContent)
352
- await session.send(result.videoContent);
306
+ if (r.textContent)
307
+ await sendWithTimeout(session, r.textContent, config.videoSendTimeout);
308
+ if (r.videoContent)
309
+ await sendWithTimeout(session, r.videoContent, config.videoSendTimeout);
353
310
  }
354
311
  catch (e) {
355
- if (!config.ignoreSendError) {
356
- ctx.logger.warn(`发送消息失败: ${e.message}`);
357
- }
312
+ if (!config.ignoreSendError)
313
+ ctx.logger.error(`发送消息失败: ${e.message}`);
358
314
  }
359
315
  }
360
316
  }
361
317
  ctx.on('message', async (session) => {
362
- try {
363
- if (!config.enable)
364
- return;
365
- const content = session.content.trim();
366
- if (!hasPlatformKeyword(content))
367
- return;
368
- const urls = extractUrl(content);
369
- if (urls.length === 0)
370
- return;
371
- const sessionKey = `${session.platform}:${session.userId}:${session.channelId}`;
372
- if (linkBuffer.has(sessionKey)) {
373
- const existing = linkBuffer.get(sessionKey);
374
- existing.urls.push(...urls);
375
- clearTimeout(existing.timer);
376
- existing.timer = setTimeout(() => flushBuffer(session), Math.max(0, config.messageBufferDelay * 1000));
377
- linkBuffer.set(sessionKey, existing);
378
- return;
379
- }
380
- let tipMsgId;
381
- if (config.showWaitingTip) {
382
- const tipText = config.waitingTipText;
383
- const tipMsg = await session.send(tipText).catch(e => {
384
- if (!config.ignoreSendError)
385
- ctx.logger.warn(`发送等待提示失败: ${e.message}`);
386
- return null;
387
- });
388
- tipMsgId = tipMsg?.messageId || tipMsg?.id || tipMsg;
389
- }
390
- linkBuffer.set(sessionKey, {
391
- urls,
392
- timer: setTimeout(() => flushBuffer(session), Math.max(0, config.messageBufferDelay * 1000)),
393
- tipMsgId
394
- });
318
+ if (!config.enable)
319
+ return;
320
+ const content = session.content.trim();
321
+ if (!hasPlatformKeyword(content))
322
+ return;
323
+ const urls = extractUrl(content);
324
+ if (urls.length === 0)
325
+ return;
326
+ const key = `${session.platform}:${session.userId}:${session.channelId}`;
327
+ if (linkBuffer.has(key)) {
328
+ const b = linkBuffer.get(key);
329
+ b.urls.push(...urls);
330
+ clearTimeout(b.timer);
331
+ b.timer = setTimeout(() => flush(session), Math.max(0, config.messageBufferDelay * 1000));
332
+ linkBuffer.set(key, b);
333
+ return;
395
334
  }
396
- catch (e) {
397
- ctx.logger.error(`消息处理异常: ${e.message}`);
398
- const sessionKey = `${session.platform}:${session.userId}:${session.channelId}`;
399
- if (linkBuffer.has(sessionKey)) {
400
- clearTimeout(linkBuffer.get(sessionKey).timer);
401
- linkBuffer.delete(sessionKey);
335
+ let tipId;
336
+ if (config.showWaitingTip) {
337
+ const m = await sendWithTimeout(session, config.waitingTipText, config.videoSendTimeout);
338
+ if (m) {
339
+ tipId = m?.messageId || m?.id || m;
402
340
  }
403
341
  }
342
+ linkBuffer.set(key, {
343
+ urls,
344
+ timer: setTimeout(() => flush(session), Math.max(0, config.messageBufferDelay * 1000)),
345
+ tipMsgId: tipId
346
+ });
404
347
  });
405
348
  setInterval(() => {
406
349
  const now = Date.now();
407
- processed.forEach((timestamp, hash) => {
408
- if (now - timestamp > 86400000) {
409
- processed.delete(hash);
410
- }
411
- });
350
+ processed.forEach((t, h) => now - t > 86400000 && processed.delete(h));
351
+ }, 3600000);
352
+ setInterval(() => {
412
353
  const tempDir = path_1.default.join(process.cwd(), 'temp_videos');
413
354
  if (fs_1.default.existsSync(tempDir)) {
414
- fs_1.default.readdirSync(tempDir).forEach(file => {
355
+ const files = fs_1.default.readdirSync(tempDir);
356
+ const now = Date.now();
357
+ files.forEach(file => {
358
+ const filePath = path_1.default.join(tempDir, file);
415
359
  try {
416
- const filePath = path_1.default.join(tempDir, file);
417
- const stat = fs_1.default.statSync(filePath);
418
- if (now - stat.ctimeMs > 3600000) {
360
+ const stats = fs_1.default.statSync(filePath);
361
+ if (now - stats.mtimeMs > 3600000) {
419
362
  fs_1.default.unlinkSync(filePath);
420
363
  }
421
364
  }
@@ -424,6 +367,6 @@ function apply(ctx, config) {
424
367
  }
425
368
  });
426
369
  }
427
- }, 3600000);
428
- ctx.logger.info('视频解析插件加载完成,缓存已清空');
370
+ }, 1800000);
371
+ ctx.logger.info('视频解析插件已加载完成');
429
372
  }
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.1.9",
4
+ "version": "0.2.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [