koishi-plugin-video-parser-all 0.8.5 → 0.8.7
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 +2 -20
- package/lib/index.js +20 -248
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export declare const name = "video-parser-all";
|
|
|
3
3
|
export declare const Config: Schema<{
|
|
4
4
|
enable?: boolean | null | undefined;
|
|
5
5
|
botName?: string | null | undefined;
|
|
6
|
+
showWaitingTip?: boolean | null | undefined;
|
|
6
7
|
debug?: boolean | null | undefined;
|
|
7
|
-
debugFile?: boolean | null | undefined;
|
|
8
8
|
} & import("cosmokit").Dict & {
|
|
9
9
|
unifiedMessageFormat?: string | null | undefined;
|
|
10
10
|
} & {
|
|
@@ -22,26 +22,17 @@ export declare const Config: Schema<{
|
|
|
22
22
|
retryInterval?: number | null | undefined;
|
|
23
23
|
} & {
|
|
24
24
|
enableForward?: boolean | null | undefined;
|
|
25
|
-
downloadVideoBeforeSend?: boolean | null | undefined;
|
|
26
|
-
maxVideoSize?: number | null | undefined;
|
|
27
|
-
downloadThreads?: number | null | undefined;
|
|
28
|
-
} & {
|
|
29
|
-
messageBufferDelay?: number | null | undefined;
|
|
30
|
-
} & {
|
|
31
|
-
autoClearCacheInterval?: number | null | undefined;
|
|
32
25
|
} & {
|
|
33
26
|
waitingTipText?: string | null | undefined;
|
|
34
|
-
duplicateLinkText?: string | null | undefined;
|
|
35
27
|
unsupportedPlatformText?: string | null | undefined;
|
|
36
28
|
invalidLinkText?: string | null | undefined;
|
|
37
|
-
cacheClearedText?: string | null | undefined;
|
|
38
29
|
parseErrorPrefix?: string | null | undefined;
|
|
39
30
|
parseErrorItemFormat?: string | null | undefined;
|
|
40
31
|
}, {
|
|
41
32
|
enable: boolean;
|
|
42
33
|
botName: string;
|
|
34
|
+
showWaitingTip: boolean;
|
|
43
35
|
debug: boolean;
|
|
44
|
-
debugFile: boolean;
|
|
45
36
|
} & import("cosmokit").Dict & {
|
|
46
37
|
unifiedMessageFormat: string;
|
|
47
38
|
} & {
|
|
@@ -59,19 +50,10 @@ export declare const Config: Schema<{
|
|
|
59
50
|
retryInterval: number;
|
|
60
51
|
} & {
|
|
61
52
|
enableForward: boolean;
|
|
62
|
-
downloadVideoBeforeSend: boolean;
|
|
63
|
-
maxVideoSize: number;
|
|
64
|
-
downloadThreads: number;
|
|
65
|
-
} & {
|
|
66
|
-
messageBufferDelay: number;
|
|
67
|
-
} & {
|
|
68
|
-
autoClearCacheInterval: number;
|
|
69
53
|
} & {
|
|
70
54
|
waitingTipText: string;
|
|
71
|
-
duplicateLinkText: string;
|
|
72
55
|
unsupportedPlatformText: string;
|
|
73
56
|
invalidLinkText: string;
|
|
74
|
-
cacheClearedText: string;
|
|
75
57
|
parseErrorPrefix: string;
|
|
76
58
|
parseErrorItemFormat: string;
|
|
77
59
|
}>;
|
package/lib/index.js
CHANGED
|
@@ -7,21 +7,16 @@ exports.Config = exports.name = void 0;
|
|
|
7
7
|
exports.apply = apply;
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
-
const fs_1 = __importDefault(require("fs"));
|
|
12
|
-
const path_1 = __importDefault(require("path"));
|
|
13
|
-
const promises_1 = require("stream/promises");
|
|
14
|
-
const worker_threads_1 = require("worker_threads");
|
|
15
10
|
exports.name = 'video-parser-all';
|
|
16
11
|
exports.Config = koishi_1.Schema.intersect([
|
|
17
12
|
koishi_1.Schema.object({
|
|
18
13
|
enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
|
|
19
14
|
botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发消息中显示的机器人名称'),
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
|
|
16
|
+
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
|
|
22
17
|
}).description('基础设置'),
|
|
23
18
|
koishi_1.Schema.object({
|
|
24
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(
|
|
19
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`\${标题}\n\${作者}\n\${简介}\n点赞:\${点赞数}\n收藏:\${收藏数}\n转发:\${转发数}\n播放:\${播放数}\n评论:\${评论数}`).description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID} ${视频链接} ${封面} ${音乐作者} ${音乐标题}'),
|
|
25
20
|
}).description('消息格式设置'),
|
|
26
21
|
koishi_1.Schema.object({
|
|
27
22
|
showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
|
|
@@ -41,50 +36,23 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
41
36
|
}).description('错误与重试设置'),
|
|
42
37
|
koishi_1.Schema.object({
|
|
43
38
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
|
|
44
|
-
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频到本地'),
|
|
45
|
-
maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('视频下载大小限制(MB,0 为不限制)'),
|
|
46
|
-
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0 为单线程)'),
|
|
47
39
|
}).description('发送方式设置'),
|
|
48
|
-
koishi_1.Schema.object({
|
|
49
|
-
messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒)'),
|
|
50
|
-
}).description('消息缓冲'),
|
|
51
|
-
koishi_1.Schema.object({
|
|
52
|
-
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0 为关闭)'),
|
|
53
|
-
}).description('缓存清理'),
|
|
54
40
|
koishi_1.Schema.object({
|
|
55
41
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示'),
|
|
56
|
-
duplicateLinkText: koishi_1.Schema.string().default('请勿重复解析相同链接').description('重复链接提示'),
|
|
57
42
|
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示'),
|
|
58
43
|
invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示(parse 指令)'),
|
|
59
|
-
cacheClearedText: koishi_1.Schema.string().default('✅ 缓存已清空').description('缓存清理提示'),
|
|
60
44
|
parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('解析失败消息前缀'),
|
|
61
45
|
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败格式,可用 ${url} ${msg}'),
|
|
62
46
|
}).description('界面文字设置'),
|
|
63
47
|
]);
|
|
64
|
-
const processed = new Map();
|
|
65
|
-
const linkBuffer = new Map();
|
|
66
48
|
const logger = new koishi_1.Logger(exports.name);
|
|
67
49
|
let debugEnabled = false;
|
|
68
|
-
let debugFileEnabled = false;
|
|
69
|
-
let debugStream = null;
|
|
70
50
|
function debugLog(level, ...args) {
|
|
71
51
|
if (!debugEnabled)
|
|
72
52
|
return;
|
|
73
53
|
const timestamp = new Date().toISOString();
|
|
74
54
|
const message = `[${timestamp}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')}`;
|
|
75
55
|
logger.info(message);
|
|
76
|
-
if (debugFileEnabled && debugStream) {
|
|
77
|
-
debugStream.write(message + '\n');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function initDebug(enabled, fileEnabled) {
|
|
81
|
-
debugEnabled = enabled;
|
|
82
|
-
debugFileEnabled = fileEnabled;
|
|
83
|
-
if (fileEnabled && enabled) {
|
|
84
|
-
const logPath = path_1.default.join(process.cwd(), 'debug.log');
|
|
85
|
-
debugStream = fs_1.default.createWriteStream(logPath, { flags: 'a' });
|
|
86
|
-
debugStream.write(`\n=== Debug session started at ${new Date().toISOString()} ===\n`);
|
|
87
|
-
}
|
|
88
56
|
}
|
|
89
57
|
const PLATFORM_KEYWORDS = {
|
|
90
58
|
bilibili: ['bilibili', 'b23', 'www.bilibili.com', 'm.bilibili.com', 'b23.tv', 't.bilibili.com', 'bilibili.com/video', 'bilibili.com/opus', 'bilibili.com/bangumi'],
|
|
@@ -118,103 +86,6 @@ function getErrorMessage(error) {
|
|
|
118
86
|
return error.message;
|
|
119
87
|
return String(error);
|
|
120
88
|
}
|
|
121
|
-
async function getFileSize(url, userAgent) {
|
|
122
|
-
try {
|
|
123
|
-
const response = await axios_1.default.head(url, {
|
|
124
|
-
timeout: 10000,
|
|
125
|
-
headers: { 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
|
|
126
|
-
});
|
|
127
|
-
const contentLength = response.headers['content-length'];
|
|
128
|
-
if (contentLength)
|
|
129
|
-
return Math.round(Number(contentLength) / 1024 / 1024 * 100) / 100;
|
|
130
|
-
}
|
|
131
|
-
catch (error) { }
|
|
132
|
-
return 0;
|
|
133
|
-
}
|
|
134
|
-
async function downloadVideoThread(workerData) {
|
|
135
|
-
return new Promise((resolve, reject) => {
|
|
136
|
-
const worker = new worker_threads_1.Worker(__filename, { workerData });
|
|
137
|
-
worker.on('message', resolve);
|
|
138
|
-
worker.on('error', reject);
|
|
139
|
-
worker.on('exit', (code) => {
|
|
140
|
-
if (code !== 0)
|
|
141
|
-
reject(new Error(`下载线程异常退出,代码:${code}`));
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
if (!worker_threads_1.isMainThread) {
|
|
146
|
-
const { url, start, end, filename, userAgent } = worker_threads_1.workerData;
|
|
147
|
-
const filePath = path_1.default.join(process.cwd(), 'temp_videos', `${filename}_${start}_${end}.part`);
|
|
148
|
-
(0, axios_1.default)({
|
|
149
|
-
url,
|
|
150
|
-
method: 'GET',
|
|
151
|
-
responseType: 'stream',
|
|
152
|
-
timeout: 60000,
|
|
153
|
-
headers: {
|
|
154
|
-
'User-Agent': userAgent,
|
|
155
|
-
'Range': `bytes=${start}-${end}`
|
|
156
|
-
}
|
|
157
|
-
}).then(response => {
|
|
158
|
-
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
159
|
-
response.data.pipe(writeStream);
|
|
160
|
-
writeStream.on('finish', () => worker_threads_1.parentPort?.postMessage({ success: true, filePath, start, end }));
|
|
161
|
-
writeStream.on('error', (error) => worker_threads_1.parentPort?.postMessage({ success: false, error: error.message }));
|
|
162
|
-
}).catch((error) => worker_threads_1.parentPort?.postMessage({ success: false, error: error.message }));
|
|
163
|
-
}
|
|
164
|
-
async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
165
|
-
const dir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
166
|
-
if (!fs_1.default.existsSync(dir))
|
|
167
|
-
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
168
|
-
const filePath = path_1.default.join(dir, `${filename}.mp4`);
|
|
169
|
-
try {
|
|
170
|
-
if (url.endsWith('.m4a') || url.endsWith('.mp3'))
|
|
171
|
-
return { filePath: '', success: false };
|
|
172
|
-
const fileSize = await getFileSize(url, userAgent);
|
|
173
|
-
if (maxSize > 0 && fileSize > maxSize)
|
|
174
|
-
return { filePath: '', success: false };
|
|
175
|
-
if (threads <= 0 || fileSize === 0) {
|
|
176
|
-
const response = await (0, axios_1.default)({
|
|
177
|
-
url,
|
|
178
|
-
method: 'GET',
|
|
179
|
-
responseType: 'stream',
|
|
180
|
-
timeout: 60000,
|
|
181
|
-
headers: { 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
|
|
182
|
-
});
|
|
183
|
-
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
184
|
-
await (0, promises_1.pipeline)(response.data, writeStream);
|
|
185
|
-
return { filePath, success: true };
|
|
186
|
-
}
|
|
187
|
-
const totalSize = fileSize * 1024 * 1024;
|
|
188
|
-
const chunkSize = Math.ceil(totalSize / threads);
|
|
189
|
-
const promises = [];
|
|
190
|
-
for (let i = 0; i < threads; i++) {
|
|
191
|
-
const start = i * chunkSize;
|
|
192
|
-
const end = i === threads - 1 ? totalSize - 1 : start + chunkSize - 1;
|
|
193
|
-
promises.push(downloadVideoThread({ url, start, end, filename, userAgent }));
|
|
194
|
-
}
|
|
195
|
-
const results = await Promise.all(promises);
|
|
196
|
-
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
197
|
-
for (const result of results) {
|
|
198
|
-
if (!result.success)
|
|
199
|
-
throw new Error(result.error);
|
|
200
|
-
const readStream = fs_1.default.createReadStream(result.filePath);
|
|
201
|
-
await (0, promises_1.pipeline)(readStream, writeStream, { end: false });
|
|
202
|
-
fs_1.default.unlinkSync(result.filePath);
|
|
203
|
-
}
|
|
204
|
-
writeStream.end();
|
|
205
|
-
return { filePath, success: true };
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
if (fs_1.default.existsSync(filePath))
|
|
209
|
-
fs_1.default.unlinkSync(filePath);
|
|
210
|
-
const partFiles = fs_1.default.readdirSync(dir).filter(file => file.startsWith(`${filename}_`) && file.endsWith('.part'));
|
|
211
|
-
partFiles.forEach(file => { try {
|
|
212
|
-
fs_1.default.unlinkSync(path_1.default.join(dir, file));
|
|
213
|
-
}
|
|
214
|
-
catch (e) { } });
|
|
215
|
-
return { filePath: '', success: false };
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
89
|
function extractUrl(content) {
|
|
219
90
|
const urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
|
|
220
91
|
return urlMatches.filter(url => {
|
|
@@ -361,14 +232,12 @@ function parseApiResponse(raw, maxDescLen) {
|
|
|
361
232
|
else if (extra.create_time) {
|
|
362
233
|
publishTime = extra.create_time * 1000;
|
|
363
234
|
}
|
|
364
|
-
|
|
235
|
+
return {
|
|
365
236
|
type, title, desc, author, uid, avatar, cover,
|
|
366
237
|
video, videos, images, live_photo, music,
|
|
367
238
|
like, comment, collect, share, play,
|
|
368
239
|
duration, publishTime
|
|
369
240
|
};
|
|
370
|
-
debugLog('DEBUG', '解析后的数据:', result);
|
|
371
|
-
return result;
|
|
372
241
|
}
|
|
373
242
|
function generateFormattedText(p, format) {
|
|
374
243
|
const vars = {
|
|
@@ -393,24 +262,7 @@ function generateFormattedText(p, format) {
|
|
|
393
262
|
for (const [key, value] of Object.entries(vars)) {
|
|
394
263
|
result = result.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value);
|
|
395
264
|
}
|
|
396
|
-
|
|
397
|
-
debugLog('DEBUG', '生成格式化文本:', final);
|
|
398
|
-
return final;
|
|
399
|
-
}
|
|
400
|
-
function clearAllCache() {
|
|
401
|
-
processed.clear();
|
|
402
|
-
linkBuffer.forEach(buf => clearTimeout(buf.timer));
|
|
403
|
-
linkBuffer.clear();
|
|
404
|
-
const tempDir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
405
|
-
if (fs_1.default.existsSync(tempDir)) {
|
|
406
|
-
fs_1.default.readdirSync(tempDir).forEach(file => {
|
|
407
|
-
try {
|
|
408
|
-
fs_1.default.unlinkSync(path_1.default.join(tempDir, file));
|
|
409
|
-
}
|
|
410
|
-
catch (error) { }
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
return true;
|
|
265
|
+
return result.replace(/^\s*\n/gm, '').trim();
|
|
414
266
|
}
|
|
415
267
|
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
416
268
|
function buildForwardNode(session, content, botName) {
|
|
@@ -424,19 +276,15 @@ function buildForwardNode(session, content, botName) {
|
|
|
424
276
|
return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
|
|
425
277
|
}
|
|
426
278
|
function apply(ctx, config) {
|
|
427
|
-
|
|
279
|
+
debugEnabled = config.debug || false;
|
|
428
280
|
debugLog('INFO', '插件初始化开始');
|
|
429
|
-
debugLog('INFO', '当前配置:', config);
|
|
430
281
|
const texts = {
|
|
431
282
|
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
432
|
-
duplicateLinkText: config.duplicateLinkText || '请勿重复解析相同链接',
|
|
433
283
|
unsupportedPlatformText: config.unsupportedPlatformText || '不支持该平台链接',
|
|
434
284
|
invalidLinkText: config.invalidLinkText || '无效的视频链接',
|
|
435
|
-
cacheClearedText: config.cacheClearedText || '✅ 缓存已清空',
|
|
436
285
|
parseErrorPrefix: config.parseErrorPrefix || '❌ 解析失败:',
|
|
437
286
|
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
438
287
|
};
|
|
439
|
-
clearAllCache();
|
|
440
288
|
const http = axios_1.default.create({
|
|
441
289
|
timeout: config.timeout,
|
|
442
290
|
headers: {
|
|
@@ -453,8 +301,7 @@ function apply(ctx, config) {
|
|
|
453
301
|
params: { url },
|
|
454
302
|
timeout: config.timeout
|
|
455
303
|
});
|
|
456
|
-
debugLog('
|
|
457
|
-
debugLog('DEBUG', 'API完整响应:', res.data);
|
|
304
|
+
debugLog('DEBUG', `API响应: ${JSON.stringify(res.data)}`);
|
|
458
305
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
459
306
|
return parseApiResponse(res.data, config.maxDescLength);
|
|
460
307
|
}
|
|
@@ -469,39 +316,23 @@ function apply(ctx, config) {
|
|
|
469
316
|
throw new Error('API请求全部失败');
|
|
470
317
|
}
|
|
471
318
|
async function parseUrl(url) {
|
|
472
|
-
debugLog('INFO', `开始解析链接: ${url}`);
|
|
473
319
|
const realUrl = await resolveShortUrl(url);
|
|
474
|
-
debugLog('DEBUG', `重定向后的URL: ${realUrl}`);
|
|
475
320
|
const platform = getPlatformType(realUrl);
|
|
476
321
|
if (!platform) {
|
|
477
|
-
debugLog('WARN', `不支持的平台: ${realUrl}`);
|
|
478
322
|
return { success: false, msg: texts.unsupportedPlatformText };
|
|
479
323
|
}
|
|
480
|
-
const
|
|
481
|
-
let lastError = null;
|
|
482
|
-
for (const candidate of candidates) {
|
|
324
|
+
for (const candidate of [url, realUrl]) {
|
|
483
325
|
try {
|
|
484
326
|
const info = await fetchApi(candidate);
|
|
485
|
-
debugLog('INFO', `解析成功: ${info.title}`);
|
|
486
327
|
return { success: true, data: info };
|
|
487
328
|
}
|
|
488
329
|
catch (error) {
|
|
489
|
-
|
|
490
|
-
debugLog('ERROR', `候选链接解析失败: ${candidate} => ${lastError}`);
|
|
330
|
+
debugLog('ERROR', `候选链接解析失败: ${candidate}`);
|
|
491
331
|
}
|
|
492
332
|
}
|
|
493
|
-
return { success: false, msg:
|
|
333
|
+
return { success: false, msg: '解析失败' };
|
|
494
334
|
}
|
|
495
|
-
async function processSingleUrl(
|
|
496
|
-
debugLog('INFO', `处理单个URL: ${url}, 用户: ${session.userId}`);
|
|
497
|
-
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
498
|
-
const now = Date.now();
|
|
499
|
-
const last = processed.get(hash);
|
|
500
|
-
if (last && (now - last) < config.sameLinkInterval * 1000) {
|
|
501
|
-
debugLog('WARN', `重复解析: ${url}`);
|
|
502
|
-
return { success: false, msg: texts.duplicateLinkText };
|
|
503
|
-
}
|
|
504
|
-
processed.set(hash, now);
|
|
335
|
+
async function processSingleUrl(url) {
|
|
505
336
|
const result = await parseUrl(url);
|
|
506
337
|
if (!result.success)
|
|
507
338
|
return result;
|
|
@@ -509,13 +340,11 @@ function apply(ctx, config) {
|
|
|
509
340
|
return { success: true, data: { text, parsed: result.data } };
|
|
510
341
|
}
|
|
511
342
|
async function sendWithTimeout(session, content) {
|
|
512
|
-
debugLog('DEBUG', `发送消息: ${JSON.stringify(content)}`);
|
|
513
343
|
if (config.videoSendTimeout <= 0) {
|
|
514
344
|
try {
|
|
515
345
|
return await session.send(content);
|
|
516
346
|
}
|
|
517
347
|
catch (err) {
|
|
518
|
-
debugLog('ERROR', `发送消息失败: ${getErrorMessage(err)}`);
|
|
519
348
|
if (!config.ignoreSendError)
|
|
520
349
|
throw err;
|
|
521
350
|
return null;
|
|
@@ -528,24 +357,16 @@ function apply(ctx, config) {
|
|
|
528
357
|
]);
|
|
529
358
|
}
|
|
530
359
|
catch (err) {
|
|
531
|
-
debugLog('ERROR', `发送消息超时或失败: ${getErrorMessage(err)}`);
|
|
532
360
|
if (!config.ignoreSendError)
|
|
533
361
|
throw err;
|
|
534
362
|
return null;
|
|
535
363
|
}
|
|
536
364
|
}
|
|
537
|
-
async function flush(session,
|
|
538
|
-
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
539
|
-
const buffer = linkBuffer.get(key);
|
|
540
|
-
const urls = manualUrls || buffer?.urls || [];
|
|
541
|
-
if (buffer) {
|
|
542
|
-
clearTimeout(buffer.timer);
|
|
543
|
-
linkBuffer.delete(key);
|
|
544
|
-
}
|
|
365
|
+
async function flush(session, urls) {
|
|
545
366
|
const items = [];
|
|
546
367
|
const errors = [];
|
|
547
368
|
for (const url of urls) {
|
|
548
|
-
const res = await processSingleUrl(
|
|
369
|
+
const res = await processSingleUrl(url);
|
|
549
370
|
if (res.success) {
|
|
550
371
|
items.push(res.data);
|
|
551
372
|
}
|
|
@@ -568,9 +389,7 @@ function apply(ctx, config) {
|
|
|
568
389
|
for (const item of items) {
|
|
569
390
|
const p = item.parsed;
|
|
570
391
|
const text = item.text;
|
|
571
|
-
debugLog('INFO', `开始发送内容,类型: ${p.type}, 标题: ${p.title}`);
|
|
572
392
|
if (text && config.showImageText) {
|
|
573
|
-
debugLog('DEBUG', '发送文本消息');
|
|
574
393
|
if (enableForward)
|
|
575
394
|
forwardMessages.push(buildForwardNode(session, text, botName));
|
|
576
395
|
else {
|
|
@@ -579,7 +398,6 @@ function apply(ctx, config) {
|
|
|
579
398
|
}
|
|
580
399
|
}
|
|
581
400
|
if (p.cover && p.type !== 'live_photo') {
|
|
582
|
-
debugLog('DEBUG', '发送封面图片:', p.cover);
|
|
583
401
|
if (enableForward)
|
|
584
402
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
585
403
|
else {
|
|
@@ -588,41 +406,21 @@ function apply(ctx, config) {
|
|
|
588
406
|
}
|
|
589
407
|
}
|
|
590
408
|
if (p.video && config.showVideoFile && (p.type === 'video' || p.type === 'live')) {
|
|
591
|
-
const
|
|
592
|
-
if (config.downloadVideoBeforeSend) {
|
|
593
|
-
const fname = crypto_1.default.createHash('md5').update(p.video).digest('hex');
|
|
594
|
-
const dl = await downloadVideo(p.video, fname, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
595
|
-
if (dl.success) {
|
|
596
|
-
debugLog('INFO', `视频下载成功,发送文件: ${dl.filePath}`);
|
|
597
|
-
return koishi_1.h.file(dl.filePath);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
debugLog('INFO', `发送视频链接: ${p.video}`);
|
|
601
|
-
return koishi_1.h.video(p.video);
|
|
602
|
-
};
|
|
409
|
+
const videoMsg = koishi_1.h.video(p.video);
|
|
603
410
|
if (enableForward) {
|
|
604
|
-
|
|
605
|
-
forwardMessages.push(buildForwardNode(session, vMsg, botName));
|
|
411
|
+
forwardMessages.push(buildForwardNode(session, videoMsg, botName));
|
|
606
412
|
}
|
|
607
413
|
else {
|
|
608
414
|
try {
|
|
609
|
-
|
|
610
|
-
await sendWithTimeout(session, vMsg);
|
|
611
|
-
}
|
|
612
|
-
catch (e) {
|
|
613
|
-
debugLog('ERROR', `发送视频失败: ${getErrorMessage(e)},尝试直接发送链接`);
|
|
614
|
-
try {
|
|
615
|
-
await sendWithTimeout(session, koishi_1.h.video(p.video));
|
|
616
|
-
}
|
|
617
|
-
catch { }
|
|
415
|
+
await sendWithTimeout(session, videoMsg);
|
|
618
416
|
}
|
|
417
|
+
catch { }
|
|
619
418
|
await delay(500);
|
|
620
419
|
}
|
|
621
420
|
}
|
|
622
421
|
if (p.type === 'image' || p.type === 'live_photo') {
|
|
623
422
|
const mediaList = [];
|
|
624
423
|
if (p.type === 'live_photo' && p.live_photo?.length) {
|
|
625
|
-
debugLog('INFO', `发送实况图集,共 ${p.live_photo.length} 张`);
|
|
626
424
|
for (const lp of p.live_photo) {
|
|
627
425
|
if (lp.image)
|
|
628
426
|
mediaList.push({ type: 'image', url: lp.image });
|
|
@@ -631,7 +429,6 @@ function apply(ctx, config) {
|
|
|
631
429
|
}
|
|
632
430
|
}
|
|
633
431
|
else if (p.images?.length) {
|
|
634
|
-
debugLog('INFO', `发送图集,共 ${p.images.length} 张`);
|
|
635
432
|
p.images.forEach(url => mediaList.push({ type: 'image', url }));
|
|
636
433
|
}
|
|
637
434
|
if (enableForward) {
|
|
@@ -642,25 +439,20 @@ function apply(ctx, config) {
|
|
|
642
439
|
}
|
|
643
440
|
else {
|
|
644
441
|
for (const m of mediaList) {
|
|
645
|
-
debugLog('DEBUG', `发送${m.type}: ${m.url}`);
|
|
646
442
|
try {
|
|
647
443
|
await sendWithTimeout(session, m.type === 'image' ? koishi_1.h.image(m.url) : koishi_1.h.video(m.url));
|
|
648
444
|
await delay(200);
|
|
649
445
|
}
|
|
650
|
-
catch
|
|
651
|
-
debugLog('ERROR', `发送${m.type}失败: ${getErrorMessage(e)}`);
|
|
652
|
-
}
|
|
446
|
+
catch { }
|
|
653
447
|
}
|
|
654
448
|
}
|
|
655
449
|
}
|
|
656
450
|
}
|
|
657
451
|
if (enableForward && forwardMessages.length) {
|
|
658
|
-
debugLog('INFO', `合并转发消息,共 ${forwardMessages.length} 条`);
|
|
659
452
|
try {
|
|
660
453
|
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)));
|
|
661
454
|
}
|
|
662
|
-
catch
|
|
663
|
-
debugLog('ERROR', `合并转发失败,降级逐条发送: ${getErrorMessage(e)}`);
|
|
455
|
+
catch {
|
|
664
456
|
for (const node of forwardMessages) {
|
|
665
457
|
try {
|
|
666
458
|
await sendWithTimeout(session, node.data.content);
|
|
@@ -675,13 +467,9 @@ function apply(ctx, config) {
|
|
|
675
467
|
if (!config.enable)
|
|
676
468
|
return;
|
|
677
469
|
const content = session.content?.trim() || '';
|
|
678
|
-
debugLog('INFO', `收到消息: "${content}"`);
|
|
679
470
|
const urls = extractUrl(content);
|
|
680
|
-
if (!urls.length)
|
|
681
|
-
debugLog('DEBUG', '消息中未检测到平台链接');
|
|
471
|
+
if (!urls.length)
|
|
682
472
|
return;
|
|
683
|
-
}
|
|
684
|
-
debugLog('INFO', '检测到链接:', urls);
|
|
685
473
|
if (config.showWaitingTip) {
|
|
686
474
|
try {
|
|
687
475
|
await sendWithTimeout(session, texts.waitingTipText);
|
|
@@ -691,7 +479,6 @@ function apply(ctx, config) {
|
|
|
691
479
|
await flush(session, urls);
|
|
692
480
|
});
|
|
693
481
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
694
|
-
debugLog('INFO', `手动解析指令: ${url}`);
|
|
695
482
|
const us = extractUrl(url);
|
|
696
483
|
if (!us.length) {
|
|
697
484
|
await sendWithTimeout(session, texts.invalidLinkText);
|
|
@@ -699,20 +486,5 @@ function apply(ctx, config) {
|
|
|
699
486
|
}
|
|
700
487
|
await flush(session, us);
|
|
701
488
|
});
|
|
702
|
-
ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
|
|
703
|
-
clearAllCache();
|
|
704
|
-
await sendWithTimeout(session, texts.cacheClearedText);
|
|
705
|
-
});
|
|
706
|
-
setInterval(() => {
|
|
707
|
-
const now = Date.now();
|
|
708
|
-
processed.forEach((t, h) => { if (now - t > 86400000)
|
|
709
|
-
processed.delete(h); });
|
|
710
|
-
}, 3600000);
|
|
711
|
-
if (config.autoClearCacheInterval > 0) {
|
|
712
|
-
setInterval(() => {
|
|
713
|
-
clearAllCache();
|
|
714
|
-
debugLog('INFO', '自动清理缓存');
|
|
715
|
-
}, config.autoClearCacheInterval * 60 * 1000);
|
|
716
|
-
}
|
|
717
489
|
debugLog('INFO', '插件初始化完成');
|
|
718
490
|
}
|
package/package.json
CHANGED