koishi-plugin-video-parser-all 0.2.2 → 0.2.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.
package/lib/index.d.ts CHANGED
@@ -3,7 +3,6 @@ export declare const name = "video-parser-all";
3
3
  export interface Config {
4
4
  enable: boolean;
5
5
  showWaitingTip: boolean;
6
- revokeWaitingTip: boolean;
7
6
  waitingTipText: string;
8
7
  sameLinkInterval: number;
9
8
  imageParseFormat: string;
@@ -20,8 +19,11 @@ export interface Config {
20
19
  messageBufferDelay: number;
21
20
  retryTimes: number;
22
21
  retryInterval: number;
23
- apiUrl: string;
22
+ apiMode: 'builtin' | 'custom';
23
+ builtinApiUrl: string;
24
+ customApiUrl: string;
24
25
  videoSendTimeout: number;
26
+ autoClearCacheInterval: number;
25
27
  }
26
28
  export declare const Config: Schema<Config>;
27
29
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -16,26 +16,28 @@ 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
19
  waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示文本'),
21
- sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接解析间隔(秒)'),
22
- // 修复:给${~~~}加转义符 \${~~~} 避免TS解析错误
23
- imageParseFormat: koishi_1.Schema.string().role('textarea').default('${标题}\n${~~~}\n${UP主}\n${~~~}\n${封面}').description(`解析结果格式
24
- 支持变量:\${标题} \${UP主} \${简介} \${点赞} \${投币} \${收藏} \${转发} \${观看} \${弹幕} \${tab} \${~~~} \${封面}`),
20
+ sameLinkInterval: koishi_1.Schema.number().default(0).min(0).description('相同链接解析间隔(秒)'),
21
+ imageParseFormat: koishi_1.Schema.string().role('textarea').default('${标题}\n${UP主}').description(`解析结果格式
22
+ 支持变量:\${标题} \${UP主} \${简介} \${tab} \${~~~}`),
25
23
  returnContent: koishi_1.Schema.object({
26
24
  showImageText: koishi_1.Schema.boolean().default(true).description('显示文本与封面'),
27
25
  showVideoUrl: koishi_1.Schema.boolean().default(false).description('显示无水印链接'),
28
26
  showVideoFile: koishi_1.Schema.boolean().default(true).description('发送视频消息'),
29
27
  }).description('返回内容设置'),
30
28
  maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
31
- timeout: koishi_1.Schema.number().default(15000).description('API请求超时(毫秒)'),
29
+ timeout: koishi_1.Schema.number().default(0).min(0).description('API请求超时(毫秒)'),
32
30
  ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送错误'),
33
31
  enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot)'),
34
32
  downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频(仅OneBot)'),
35
- messageBufferDelay: koishi_1.Schema.number().default(1).min(0).description('消息缓冲延迟(秒)'),
36
- retryTimes: koishi_1.Schema.number().default(3).min(0).description('接口重试次数'),
37
- retryInterval: koishi_1.Schema.number().default(2000).min(500).description('重试间隔(毫秒)'),
38
- apiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description(`本插件使用 BugPk-Api 公共接口
33
+ messageBufferDelay: koishi_1.Schema.number().default(0).min(0).description('消息缓冲延迟(秒)'),
34
+ retryTimes: koishi_1.Schema.number().default(0).min(0).description('接口重试次数'),
35
+ retryInterval: koishi_1.Schema.number().default(0).min(0).description('重试间隔(毫秒)'),
36
+ apiMode: koishi_1.Schema.union([
37
+ koishi_1.Schema.const('builtin').description('使用内置API'),
38
+ koishi_1.Schema.const('custom').description('使用自定义API')
39
+ ]).default('builtin').description('API使用模式'),
40
+ builtinApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description(`内置API地址
39
41
  解析失败可能原因:
40
42
  1. 视频为私密/付费/下架/限制
41
43
  2. 接口限流或维护
@@ -43,10 +45,12 @@ exports.Config = koishi_1.Schema.object({
43
45
  4. 平台链接格式更新
44
46
  注意事项:
45
47
  1. 勿频繁解析相同链接
46
- 2. 仅支持抖音/快手/B站公开视频
48
+ 2. 仅支持快手/B站/小红书/微博公开视频
47
49
  3. 公共接口不保证100%成功
48
50
  4. 失败可检查链接或稍后重试`),
49
- videoSendTimeout: koishi_1.Schema.number().default(60000).description('视频发送超时(毫秒)'),
51
+ customApiUrl: koishi_1.Schema.string().default('').description('自定义API地址(优先使用)'),
52
+ videoSendTimeout: koishi_1.Schema.number().default(0).min(0).description('视频发送超时(毫秒)'),
53
+ autoClearCacheInterval: koishi_1.Schema.number().default(0).min(0).description('自动清理缓存间隔(分钟),0表示不自动清理'),
50
54
  });
51
55
  if (!worker_threads_1.isMainThread) {
52
56
  const { url, filePath } = worker_threads_1.workerData;
@@ -77,8 +81,9 @@ const processed = new Map();
77
81
  const linkBuffer = new Map();
78
82
  const PLATFORM_KEYWORDS = {
79
83
  bilibili: ['bilibili', 'b23', 'B站', 'www.bilibili.com', 'm.bilibili.com'],
80
- kuaishou: ['kuaishou', '快手', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com'],
81
- douyin: ['douyin', '抖音', 'v.douyin.com', 'www.douyin.com', '365yg.com', 'douyinpic.com']
84
+ kuaishou: ['kuaishou', '快手', 'v.kuishou.com', 'www.kuishou.com', 'kwimgs.com'],
85
+ xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com'],
86
+ weibo: ['weibo', '微博', 'weibo.com', 'video.weibo.com', 'svproxy.168299.xyz']
82
87
  };
83
88
  function extractUrl(content) {
84
89
  const urlMatches = content.match(/https?:\/\/[^\s]+/gi) || [];
@@ -93,14 +98,25 @@ function hasPlatformKeyword(content) {
93
98
  }
94
99
  function getPlatformType(url) {
95
100
  const lower = url.toLowerCase();
96
- if (PLATFORM_KEYWORDS.douyin.some(k => lower.includes(k)))
97
- return 'douyin';
98
101
  if (PLATFORM_KEYWORDS.kuaishou.some(k => lower.includes(k)))
99
102
  return 'kuaishou';
100
103
  if (PLATFORM_KEYWORDS.bilibili.some(k => lower.includes(k)))
101
104
  return 'bilibili';
105
+ if (PLATFORM_KEYWORDS.xiaohongshu.some(k => lower.includes(k)))
106
+ return 'xiaohongshu';
107
+ if (PLATFORM_KEYWORDS.weibo.some(k => lower.includes(k)))
108
+ return 'weibo';
102
109
  return null;
103
110
  }
111
+ async function shortUrl(url) {
112
+ try {
113
+ const res = await axios_1.default.get('https://api.oick.cn/dwz/api.php', { params: { url }, timeout: 5000 });
114
+ if (res.data.code === 200)
115
+ return res.data.short_url;
116
+ }
117
+ catch { }
118
+ return url;
119
+ }
104
120
  async function downloadVideoWithThreads(url, filename) {
105
121
  return new Promise((resolve, reject) => {
106
122
  const dir = path_1.default.join(process.cwd(), 'temp_videos');
@@ -108,58 +124,39 @@ async function downloadVideoWithThreads(url, filename) {
108
124
  fs_1.default.mkdirSync(dir, { recursive: true });
109
125
  const filePath = path_1.default.join(dir, `${filename}.mp4`);
110
126
  const worker = new worker_threads_1.Worker(__filename, { workerData: { url, filePath } });
111
- worker.on('message', (result) => {
112
- if (result.success)
113
- resolve(result.filePath);
114
- else
115
- reject(new Error(result.error));
116
- });
127
+ worker.on('message', (result) => result.success ? resolve(result.filePath) : reject(new Error(result.error)));
117
128
  worker.on('error', reject);
118
- worker.on('exit', (code) => {
119
- if (code !== 0)
120
- reject(new Error('视频下载线程异常'));
121
- });
129
+ worker.on('exit', (code) => code !== 0 && reject(new Error('视频下载线程异常')));
122
130
  });
123
131
  }
124
132
  function parseData(data, maxDescLength) {
125
- let title = data.title || data.desc || '无标题';
126
- let author = data.author || data.auther || data.user?.name || '未知作者';
127
- let desc = data.desc || data.description || title || '无简介';
128
- desc = desc.slice(0, maxDescLength);
129
- let digg = data.like || data.digg || 0;
130
- let coin = data.coin || 0;
131
- let collect = data.collect || data.favorite || 0;
132
- let share = data.share || 0;
133
- let play = data.play || data.view || 0;
134
- let danmaku = data.danmaku || data.comment || 0;
135
- let cover = data.cover || data.imgurl || data.pic || '';
133
+ const type = data.type || 'video';
134
+ const title = data.title || data.desc || '无标题';
135
+ const author = data.author?.name || data.author || data.auther || data.user?.name || '未知作者';
136
+ const desc = (data.desc || data.description || title).slice(0, maxDescLength);
137
+ const cover = data.cover || data.imgurl || data.pic || '';
138
+ const images = data.images || [];
136
139
  let video = '';
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) {
140
+ if (data.url)
141
141
  video = data.url;
142
- }
143
- else if (data.video_backup && Array.isArray(data.video_backup) && data.video_backup[0]?.url) {
142
+ else if (data.videos?.[0]?.url)
143
+ video = data.videos[0].url;
144
+ else if (data.video_backup?.[0]?.url)
144
145
  video = data.video_backup[0].url;
145
- }
146
146
  if (video.endsWith('.m4a') || video.endsWith('.mp3'))
147
147
  video = '';
148
- return { title, author, desc, digg, coin, collect, share, play, danmaku, cover, video };
148
+ return { type, title, author, desc, cover, images, video };
149
149
  }
150
150
  function clearAllCache() {
151
151
  processed.clear();
152
152
  linkBuffer.forEach(b => clearTimeout(b.timer));
153
153
  linkBuffer.clear();
154
- const tempDir = path_1.default.join(process.cwd(), 'temp_videos');
155
- if (fs_1.default.existsSync(tempDir)) {
156
- fs_1.default.readdirSync(tempDir).forEach(f => {
157
- try {
158
- fs_1.default.unlinkSync(path_1.default.join(tempDir, f));
159
- }
160
- catch { }
161
- });
162
- }
154
+ const d = path_1.default.join(process.cwd(), 'temp_videos');
155
+ if (fs_1.default.existsSync(d))
156
+ fs_1.default.readdirSync(d).forEach(f => { try {
157
+ fs_1.default.unlinkSync(path_1.default.join(d, f));
158
+ }
159
+ catch { } });
163
160
  return true;
164
161
  }
165
162
  const delay = (ms) => new Promise(r => setTimeout(r, ms));
@@ -167,21 +164,24 @@ function apply(ctx, config) {
167
164
  if (!worker_threads_1.isMainThread)
168
165
  return;
169
166
  clearAllCache();
167
+ const getApiUrl = () => {
168
+ if (config.apiMode === 'custom' && config.customApiUrl.trim()) {
169
+ return config.customApiUrl.trim();
170
+ }
171
+ return config.builtinApiUrl;
172
+ };
170
173
  const http = axios_1.default.create({
171
174
  timeout: config.timeout,
172
- headers: {
173
- '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'
174
- }
175
+ headers: { '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' }
175
176
  });
176
177
  async function parse(url) {
177
- const platform = getPlatformType(url);
178
- if (!platform)
178
+ const p = getPlatformType(url);
179
+ if (!p)
179
180
  return { data: null, msg: '不支持该平台链接' };
180
181
  for (let i = 0; i <= config.retryTimes; i++) {
181
182
  try {
182
- const res = await http.get(config.apiUrl, { params: { url } });
183
- const isSuccess = (res.data.code === 200 || res.data.code === 0) && res.data.data;
184
- if (isSuccess) {
183
+ const res = await http.get(getApiUrl(), { params: { url } });
184
+ if ((res.data.code === 200 || res.data.code === 0) && res.data.data) {
185
185
  return { data: parseData(res.data.data, config.maxDescLength), msg: '解析成功' };
186
186
  }
187
187
  else {
@@ -203,163 +203,164 @@ function apply(ctx, config) {
203
203
  return { data: null, msg: '请勿重复解析' };
204
204
  }
205
205
  processed.set(hash, now);
206
- const parseResult = await parse(url);
207
- if (!parseResult.data || !parseResult.data.video) {
208
- return { data: null, msg: parseResult.msg };
209
- }
206
+ const r = await parse(url);
207
+ if (!r.data)
208
+ return { data: null, msg: r.msg };
209
+ const d = r.data;
210
210
  let text = config.imageParseFormat
211
- .replace(/\${标题}/g, parseResult.data.title)
212
- .replace(/\${UP主}/g, parseResult.data.author)
213
- .replace(/\${简介}/g, parseResult.data.desc)
214
- .replace(/\${点赞}/g, String(parseResult.data.digg))
215
- .replace(/\${投币}/g, String(parseResult.data.coin))
216
- .replace(/\${收藏}/g, String(parseResult.data.collect))
217
- .replace(/\${转发}/g, String(parseResult.data.share))
218
- .replace(/\${观看}/g, String(parseResult.data.play))
219
- .replace(/\${弹幕}/g, String(parseResult.data.danmaku))
220
- .replace(/\${tab}/g, '\t').replace(/\${~~~}/g, '\n');
221
- const contentParts = [];
222
- if (config.returnContent.showImageText) {
223
- const [b, a] = text.split('${封面}');
224
- if (b?.trim())
225
- contentParts.push(b.trim());
226
- if (parseResult.data.cover)
227
- contentParts.push(koishi_1.h.image(parseResult.data.cover));
228
- if (a?.trim())
229
- contentParts.push(a.trim());
230
- }
231
- let videoContent = '';
232
- if (config.returnContent.showVideoFile && parseResult.data.video) {
233
- if (config.downloadVideoBeforeSend && session.platform === 'onebot') {
234
- try {
235
- const name = crypto_1.default.createHash('md5').update(parseResult.data.video).digest('hex');
236
- const fp = await downloadVideoWithThreads(parseResult.data.video, name);
237
- videoContent = koishi_1.h.file(fp);
238
- }
239
- catch (e) {
240
- videoContent = koishi_1.h.video(parseResult.data.video);
241
- }
242
- }
243
- else {
244
- videoContent = koishi_1.h.video(parseResult.data.video);
245
- }
246
- }
247
- if (config.returnContent.showVideoUrl && parseResult.data.video) {
248
- contentParts.push(`🔗 无水印视频:${parseResult.data.video}`);
249
- }
250
- return {
251
- data: {
252
- textContent: contentParts.join('\n'),
253
- videoContent
254
- },
255
- msg: '解析成功'
256
- };
257
- }
258
- async function revokeTip(session, key) {
259
- if (!config.revokeWaitingTip || session.platform !== 'onebot')
260
- return;
261
- const buf = linkBuffer.get(key);
262
- if (!buf?.tipMsgId)
263
- return;
264
- try {
265
- await session.bot.deleteMessage(session.channelId, buf.tipMsgId.toString());
266
- }
267
- catch { }
211
+ .replace(/\${标题}/g, d.title)
212
+ .replace(/\${UP主}/g, d.author)
213
+ .replace(/\${简介}/g, d.desc)
214
+ .replace(/\${tab}/g, '\t')
215
+ .replace(/\${~~~}/g, '\n');
216
+ return { data: { text, cover: d.cover, images: d.images, video: d.video, type: d.type }, msg: 'ok' };
268
217
  }
269
- async function sendWithTimeout(session, content, timeout) {
270
- return Promise.race([
271
- session.send(content),
272
- new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), timeout))
273
- ]).catch(() => null);
218
+ async function sendTimeout(session, c) {
219
+ if (config.videoSendTimeout <= 0)
220
+ return session.send(c).catch(() => null);
221
+ return Promise.race([session.send(c), new Promise((_, r) => setTimeout(() => r('timeout'), config.videoSendTimeout))]).catch(() => null);
274
222
  }
275
223
  async function flush(session, manualUrls) {
276
224
  const key = `${session.platform}:${session.userId}:${session.channelId}`;
277
- let buf = linkBuffer.get(key);
278
- let urls = manualUrls || buf?.urls || [];
225
+ const buf = linkBuffer.get(key);
226
+ const urls = manualUrls || buf?.urls || [];
279
227
  if (buf) {
280
228
  clearTimeout(buf.timer);
281
229
  linkBuffer.delete(key);
282
- await revokeTip(session, key);
283
230
  }
284
- const results = [];
285
- const errorMsgs = [];
231
+ const items = [];
232
+ const errs = [];
286
233
  for (const u of urls) {
287
- const r = await processSingleUrl(session, u);
288
- if (r.data) {
289
- results.push(r.data);
234
+ const one = await processSingleUrl(session, u);
235
+ one.data ? items.push(one.data) : errs.push(`【${u.slice(0, 22)}...】:${one.msg}`);
236
+ }
237
+ const forwardMessages = [];
238
+ const botName = '视频解析机器人';
239
+ if (errs.length) {
240
+ const errorMsg = `⚠️ 部分解析失败\n${errs.join('\n')}`;
241
+ if (config.enableForward && session.platform === 'onebot') {
242
+ forwardMessages.push((0, koishi_1.h)('message', [
243
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
244
+ errorMsg
245
+ ]));
290
246
  }
291
247
  else {
292
- errorMsgs.push(`【${u.slice(0, 22)}...】:${r.msg}`);
248
+ await sendTimeout(session, errorMsg);
249
+ await delay(600);
293
250
  }
294
251
  }
295
- if (results.length === 0) {
296
- await sendWithTimeout(session, `❌ 全部解析失败\n${errorMsgs.join('\n')}`, config.videoSendTimeout);
297
- return;
298
- }
299
- if (errorMsgs.length > 0) {
300
- await sendWithTimeout(session, `⚠️ 部分解析失败\n${errorMsgs.join('\n')}`, config.videoSendTimeout);
301
- await delay(600);
302
- }
303
- if (config.enableForward && session.platform === 'onebot') {
304
- try {
305
- const forwardMessages = results.map(result => {
306
- const content = [];
307
- if (result.textContent)
308
- content.push(result.textContent);
309
- if (result.videoContent)
310
- content.push(result.videoContent);
311
- return (0, koishi_1.h)('message', [
312
- (0, koishi_1.h)('author', { id: session.selfId, name: '视频解析' }),
313
- ...content
314
- ]);
315
- });
316
- await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages), config.videoSendTimeout);
317
- return;
252
+ if (items.length === 0) {
253
+ const failMsg = `❌ 全部解析失败\n${errs.join('\n')}`;
254
+ if (config.enableForward && session.platform === 'onebot') {
255
+ forwardMessages.push((0, koishi_1.h)('message', [
256
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
257
+ failMsg
258
+ ]));
318
259
  }
319
- catch (e) {
320
- await sendWithTimeout(session, '合并转发失败,将分开发送', config.videoSendTimeout);
260
+ else {
261
+ await sendTimeout(session, failMsg);
321
262
  }
263
+ return;
322
264
  }
323
- for (const r of results) {
324
- try {
325
- if (r.textContent)
326
- await sendWithTimeout(session, r.textContent, config.videoSendTimeout);
327
- if (r.videoContent) {
328
- await delay(600);
329
- await sendWithTimeout(session, r.videoContent, config.videoSendTimeout);
265
+ for (const it of items) {
266
+ if (config.enableForward && session.platform === 'onebot') {
267
+ forwardMessages.push((0, koishi_1.h)('message', [
268
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
269
+ it.text
270
+ ]));
271
+ if (it.cover) {
272
+ forwardMessages.push((0, koishi_1.h)('message', [
273
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
274
+ koishi_1.h.image(it.cover)
275
+ ]));
276
+ }
277
+ if (it.video && config.returnContent.showVideoFile) {
278
+ let vid = koishi_1.h.video(it.video);
279
+ if (config.downloadVideoBeforeSend) {
280
+ try {
281
+ const name = crypto_1.default.createHash('md5').update(it.video).digest('hex');
282
+ vid = koishi_1.h.file(await downloadVideoWithThreads(it.video, name));
283
+ }
284
+ catch { }
285
+ }
286
+ forwardMessages.push((0, koishi_1.h)('message', [
287
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
288
+ vid
289
+ ]));
290
+ }
291
+ if (it.video && config.returnContent.showVideoUrl) {
292
+ const s = await shortUrl(it.video);
293
+ forwardMessages.push((0, koishi_1.h)('message', [
294
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
295
+ `🔗 无水印:${s}`
296
+ ]));
297
+ }
298
+ if (it.type === 'image' && it.images?.length) {
299
+ it.images.forEach(imgUrl => {
300
+ forwardMessages.push((0, koishi_1.h)('message', [
301
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
302
+ koishi_1.h.image(imgUrl)
303
+ ]));
304
+ });
330
305
  }
331
- await delay(1000);
332
306
  }
333
- catch (e) {
334
- if (!config.ignoreSendError)
335
- ctx.logger.error('发送失败');
307
+ else {
308
+ await sendTimeout(session, it.text);
309
+ await delay(300);
310
+ if (it.type === 'image' && it.images?.length) {
311
+ const msg = (0, koishi_1.h)('message', ...it.images.map(u => koishi_1.h.image(u)));
312
+ await sendTimeout(session, msg);
313
+ }
314
+ else {
315
+ if (it.cover) {
316
+ await sendTimeout(session, koishi_1.h.image(it.cover));
317
+ await delay(300);
318
+ }
319
+ if (it.video && config.returnContent.showVideoFile) {
320
+ let vid = koishi_1.h.video(it.video);
321
+ if (config.downloadVideoBeforeSend) {
322
+ try {
323
+ const name = crypto_1.default.createHash('md5').update(it.video).digest('hex');
324
+ vid = koishi_1.h.file(await downloadVideoWithThreads(it.video, name));
325
+ }
326
+ catch { }
327
+ }
328
+ await sendTimeout(session, vid);
329
+ }
330
+ if (it.video && config.returnContent.showVideoUrl) {
331
+ await delay(300);
332
+ const s = await shortUrl(it.video);
333
+ await sendTimeout(session, `🔗 无水印:${s}`);
334
+ }
335
+ }
336
+ await delay(1000);
336
337
  }
337
338
  }
339
+ if (config.enableForward && session.platform === 'onebot' && forwardMessages.length) {
340
+ const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages);
341
+ await sendTimeout(session, forwardMsg);
342
+ }
338
343
  }
339
344
  ctx.on('message', async (session) => {
340
345
  if (!config.enable)
341
346
  return;
342
- const content = session.content.trim();
343
- if (!hasPlatformKeyword(content))
344
- return;
345
- const urls = extractUrl(content);
346
- if (urls.length === 0)
347
+ const urls = extractUrl(session.content.trim());
348
+ if (!urls.length)
347
349
  return;
348
350
  const key = `${session.platform}:${session.userId}:${session.channelId}`;
349
351
  if (linkBuffer.has(key)) {
350
352
  const b = linkBuffer.get(key);
351
353
  const newUrls = urls.filter(u => !b.urls.includes(u));
352
- if (newUrls.length > 0) {
354
+ if (newUrls.length) {
353
355
  b.urls.push(...newUrls);
354
356
  clearTimeout(b.timer);
355
357
  b.timer = setTimeout(() => flush(session), config.messageBufferDelay * 1000);
356
- linkBuffer.set(key, b);
357
358
  }
358
359
  return;
359
360
  }
360
361
  let tipId;
361
362
  if (config.showWaitingTip) {
362
- const m = await sendWithTimeout(session, config.waitingTipText, config.videoSendTimeout);
363
+ const m = await sendTimeout(session, config.waitingTipText);
363
364
  tipId = m?.messageId || m?.id || m;
364
365
  }
365
366
  linkBuffer.set(key, {
@@ -368,16 +369,16 @@ function apply(ctx, config) {
368
369
  tipMsgId: tipId
369
370
  });
370
371
  });
371
- ctx.command('解析视频 <url>', '手动解析视频链接')
372
+ ctx.command('parse <url>', '手动解析视频链接')
372
373
  .action(async ({ session }, url) => {
373
374
  if (!url)
374
375
  return '请输入视频链接';
375
- const urls = extractUrl(url);
376
- if (urls.length === 0)
376
+ const us = extractUrl(url);
377
+ if (!us.length)
377
378
  return '不支持该链接';
378
- await flush(session, urls);
379
+ await flush(session, us);
379
380
  });
380
- ctx.command('清除解析缓存', '清空解析缓存与临时文件')
381
+ ctx.command('clear-cache', '清空解析缓存与临时文件')
381
382
  .action(() => {
382
383
  clearAllCache();
383
384
  return '✅ 解析缓存已清空';
@@ -387,20 +388,25 @@ function apply(ctx, config) {
387
388
  processed.forEach((t, h) => now - t > 86400000 && processed.delete(h));
388
389
  }, 3600000);
389
390
  setInterval(() => {
390
- const tempDir = path_1.default.join(process.cwd(), 'temp_videos');
391
- if (!fs_1.default.existsSync(tempDir))
391
+ const d = path_1.default.join(process.cwd(), 'temp_videos');
392
+ if (!fs_1.default.existsSync(d))
392
393
  return;
393
- const files = fs_1.default.readdirSync(tempDir);
394
394
  const now = Date.now();
395
- files.forEach(file => {
395
+ fs_1.default.readdirSync(d).forEach(f => {
396
396
  try {
397
- const st = fs_1.default.statSync(path_1.default.join(tempDir, file));
397
+ const st = fs_1.default.statSync(path_1.default.join(d, f));
398
398
  if (now - st.mtimeMs > 3600000)
399
- fs_1.default.unlinkSync(path_1.default.join(tempDir, file));
399
+ fs_1.default.unlinkSync(path_1.default.join(d, f));
400
400
  }
401
401
  catch { }
402
402
  });
403
403
  }, 1800000);
404
+ if (config.autoClearCacheInterval > 0) {
405
+ setInterval(() => {
406
+ clearAllCache();
407
+ ctx.logger.info('自动清理缓存完成');
408
+ }, config.autoClearCacheInterval * 60000);
409
+ }
404
410
  process.on('exit', clearAllCache);
405
411
  ctx.logger.info('视频解析插件已加载');
406
412
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
- "description": "Koishi 视频解析插件,支持抖音/快手/B站链接解析",
4
- "version": "0.2.2",
3
+ "description": "Koishi 视频解析插件,支持抖音/快手/B站/小红书视频链接解析",
4
+ "version": "0.2.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -35,5 +35,13 @@
35
35
  "peerDependencies": {
36
36
  "@koishijs/plugin-console": "^5.30.4",
37
37
  "koishi": "^4.18.7"
38
- }
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/Minecraft-1314/koishi-plugin-video-parser-all.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/Minecraft-1314/koishi-plugin-video-parser-all/issues"
45
+ },
46
+ "homepage": "https://github.com/Minecraft-1314/koishi-plugin-video-parser-all#readme"
39
47
  }
package/readme.md CHANGED
@@ -1,5 +1,50 @@
1
- # koishi-plugin-video-parser
1
+ # koishi-plugin-video-parser-all
2
2
 
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-video-parser?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-video-parser)
3
+ ## 项目介绍 (Project Introduction)
4
4
 
5
- video-parser
5
+ ### 中文
6
+ 这是一个为 Koishi 机器人框架开发的**多平台视频解析插件**,支持自动识别并解析抖音、快手、B站、小红书、微博等主流短视频平台链接。核心特性:
7
+ - 自动识别多平台视频链接,无需手动指定平台
8
+ - 自定义解析结果格式、返回内容类型(封面/链接/视频文件)
9
+ - 内置防重复解析、接口重试、自动缓存清理等实用功能
10
+ - 支持 OneBot 平台消息合并转发,优化展示体验
11
+ - 可切换内置 API 或自定义 API,适配不同网络环境
12
+
13
+ ### English
14
+ A multi-platform video parsing plugin for the Koishi bot framework, supporting automatic recognition and parsing of video links from Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo and other mainstream platforms. Core features:
15
+ - Auto-detect video links across platforms without manual specification
16
+ - Customize result formatting and output types (cover/link/video file)
17
+ - Built-in duplicate prevention, retry logic, auto cache cleanup
18
+ - Support OneBot message forwarding for better display experience
19
+ - Switchable between built-in and custom API for different network environments
20
+
21
+ ## 项目仓库 (Repository)
22
+ - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
23
+ - Issues: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all/issues`
24
+
25
+ ## 核心指令 (Core Commands)
26
+ ```bash
27
+ # 手动解析指定视频链接
28
+ parse <url>
29
+
30
+ # 清空解析缓存与临时视频文件
31
+ clear-cache
32
+
33
+ ## 项目贡献者 (Contributors)
34
+
35
+ | 贡献者 (Contributor) | 贡献内容 (Contribution) |
36
+ |----------------------|-------------------------|
37
+ | [donlon](https://github.com/donlon) | 项目基础框架提供 (Provided basic project framework) |
38
+ | (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
39
+
40
+ ## 许可协议 (License)
41
+
42
+ 本项目采用 MIT 许可证,详情参见 [LICENSE](LICENSE) 文件。
43
+
44
+ This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
45
+
46
+ ## 支持我们 (Support Us)
47
+
48
+ 如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们,这将是对所有贡献者最大的鼓励!
49
+
50
+ 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!