koishi-plugin-share-links-analysis 0.7.2 → 0.8.0
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/core.d.ts +1 -1
- package/lib/core.js +3 -3
- package/lib/index.js +154 -26
- package/lib/parsers/bilibili.js +5 -11
- package/lib/parsers/twitter.js +1 -1
- package/lib/parsers/xiaohongshu.js +11 -22
- package/lib/parsers/youtube.d.ts +10 -0
- package/lib/parsers/youtube.js +298 -0
- package/lib/types.d.ts +33 -3
- package/lib/utils.d.ts +8 -4
- package/lib/utils.js +97 -67
- package/package.json +1 -1
package/lib/core.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Session } from 'koishi';
|
|
2
2
|
import { Link, PluginConfig, ParsedInfo } from './types';
|
|
3
|
-
export declare const parsers_str: ("bilibili" | "xiaohongshu" | "twitter" | "xiaoheihe")[];
|
|
3
|
+
export declare const parsers_str: ("bilibili" | "xiaohongshu" | "twitter" | "xiaoheihe" | "youtube")[];
|
|
4
4
|
/**
|
|
5
5
|
* 从文本中解析出所有支持的链接
|
|
6
6
|
* @param content 消息内容
|
package/lib/core.js
CHANGED
|
@@ -42,8 +42,9 @@ const Bilibili = __importStar(require("./parsers/bilibili"));
|
|
|
42
42
|
const Xiaohongshu = __importStar(require("./parsers/xiaohongshu"));
|
|
43
43
|
const Twitter = __importStar(require("./parsers/twitter"));
|
|
44
44
|
const Xiaoheihe = __importStar(require("./parsers/xiaoheihe"));
|
|
45
|
+
const Youtube = __importStar(require("./parsers/youtube"));
|
|
45
46
|
// 定义所有支持的解析器
|
|
46
|
-
const parsers = [Bilibili, Xiaohongshu, Twitter, Xiaoheihe];
|
|
47
|
+
const parsers = [Bilibili, Xiaohongshu, Twitter, Xiaoheihe, Youtube];
|
|
47
48
|
exports.parsers_str = parsers.map(p => p.name);
|
|
48
49
|
/**
|
|
49
50
|
* 从文本中解析出所有支持的链接
|
|
@@ -69,8 +70,7 @@ function resolveLinks(content) {
|
|
|
69
70
|
async function processLink(ctx, config, link, session) {
|
|
70
71
|
for (const parser of parsers) {
|
|
71
72
|
if (parser.name == link.platform) {
|
|
72
|
-
|
|
73
|
-
ctx.logger('share-links-analysis').info(`解析平台:${parser.name},链接:${link.url}`);
|
|
73
|
+
ctx.logger('share-links-analysis').debug(`解析平台:${parser.name},链接:${link.url}`);
|
|
74
74
|
return await parser.process(ctx, config, link, session);
|
|
75
75
|
}
|
|
76
76
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/index.ts
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
37
|
exports.Config = exports.usage = exports.inject = exports.name = void 0;
|
|
5
38
|
exports.apply = apply;
|
|
6
39
|
const koishi_1 = require("koishi");
|
|
7
40
|
const core_1 = require("./core");
|
|
8
41
|
const utils_1 = require("./utils");
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
9
43
|
exports.name = 'share-links-analysis';
|
|
10
44
|
exports.inject = {
|
|
11
45
|
required: ['BiliBiliVideo', 'database', 'puppeteer'],
|
|
@@ -39,6 +73,11 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
39
73
|
sendFiles: koishi_1.Schema.boolean().default(true).description("是否发送文件(视频等)"),
|
|
40
74
|
sendLinks: koishi_1.Schema.boolean().default(false).description("是否附加直链(仅对合并发送有效)"),
|
|
41
75
|
}).description("基础设置"),
|
|
76
|
+
koishi_1.Schema.object({
|
|
77
|
+
enableCache: koishi_1.Schema.boolean().default(true).description("开启缓存(包括解析结果缓存和资源文件缓存)"),
|
|
78
|
+
cacheExpiration: koishi_1.Schema.number().default(24).description("缓存过期时间(小时)。设置为 0 则不过期。"),
|
|
79
|
+
autoCleanInterval: koishi_1.Schema.number().default(1).description("自动清理过期缓存的检查间隔(小时)。"),
|
|
80
|
+
}).description("缓存设置"),
|
|
42
81
|
koishi_1.Schema.object({
|
|
43
82
|
format: koishi_1.Schema.string().role('textarea').default(`{title}
|
|
44
83
|
{cover}
|
|
@@ -54,6 +93,9 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
54
93
|
useNumeral: koishi_1.Schema.boolean().default(true).description("使用格式化数字 (如 10000 -> 1万)"),
|
|
55
94
|
showError: koishi_1.Schema.boolean().default(false).description("当链接被阻止时提醒发送者"),
|
|
56
95
|
}).description("高级解析设置"),
|
|
96
|
+
koishi_1.Schema.object({
|
|
97
|
+
youtubeCookie: koishi_1.Schema.string().role('textarea').description("[YouTube 专用] 手动填入 Cookie 字符串。使用已登录账号的 Cookie 可大幅降低被拦截概率。<br>获取方式:浏览器F12 -> 网络 -> 刷新YouTube首页 -> 查看请求头中的 `cookie` 字段。"),
|
|
98
|
+
}).description("YouTube 设置"),
|
|
57
99
|
koishi_1.Schema.object({
|
|
58
100
|
proxy: koishi_1.Schema.string().description("代理设置"),
|
|
59
101
|
proxy_settings: koishi_1.Schema.object(Object.fromEntries(core_1.parsers_str.map(parser => [parser, koishi_1.Schema.boolean().default(false).description(`对${parser}使用代理`)]))),
|
|
@@ -68,22 +110,17 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
68
110
|
}).description('跨环境路径映射设置'),
|
|
69
111
|
koishi_1.Schema.object({
|
|
70
112
|
userAgent: koishi_1.Schema.string().description("所有 API 请求所用的 User-Agent").default("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"),
|
|
71
|
-
|
|
72
|
-
koishi_1.Schema.const('none').description('不记录'),
|
|
73
|
-
koishi_1.Schema.const('link_only').description('仅记录视频直链'),
|
|
74
|
-
koishi_1.Schema.const('full').description('记录完整调试信息'),
|
|
75
|
-
]).role('radio').default('none').description("选择后台日志记录等级"),
|
|
113
|
+
debug: koishi_1.Schema.boolean().default(false).description("开启调试模式 (输出详细日志)"),
|
|
76
114
|
}).description("调试设置"),
|
|
77
115
|
]);
|
|
78
116
|
function apply(ctx, config) {
|
|
79
|
-
//
|
|
117
|
+
// 数据库模型定义
|
|
80
118
|
ctx.model.extend('sla_cookie_cache', {
|
|
81
119
|
platform: 'string', // 平台名称,如 'xiaohongshu'
|
|
82
120
|
cookie: 'text', // 存储的 cookie 字符串
|
|
83
121
|
}, {
|
|
84
122
|
primary: 'platform' // 使用平台名称作为主键
|
|
85
123
|
});
|
|
86
|
-
// @ts-ignore
|
|
87
124
|
ctx.model.extend('sla_group_settings', {
|
|
88
125
|
guildId: 'string',
|
|
89
126
|
custom_parsers: 'json',
|
|
@@ -91,6 +128,59 @@ function apply(ctx, config) {
|
|
|
91
128
|
}, {
|
|
92
129
|
primary: 'guildId',
|
|
93
130
|
});
|
|
131
|
+
// 解析结果缓存
|
|
132
|
+
ctx.model.extend('sla_parse_cache', {
|
|
133
|
+
key: 'string', // platform + ':' + id
|
|
134
|
+
data: 'json',
|
|
135
|
+
created_at: 'double',
|
|
136
|
+
}, { primary: 'key' });
|
|
137
|
+
// 资源文件缓存 (hash)
|
|
138
|
+
ctx.model.extend('sla_file_cache', {
|
|
139
|
+
hash: 'string', // URL MD5
|
|
140
|
+
path: 'string', // 本地绝对路径
|
|
141
|
+
url: 'string',
|
|
142
|
+
created_at: 'double',
|
|
143
|
+
}, { primary: 'hash' });
|
|
144
|
+
const logger = ctx.logger('share-links-analysis');
|
|
145
|
+
// 根据配置设置日志等级
|
|
146
|
+
if (config.debug) {
|
|
147
|
+
logger.level = 3; // Debug Level
|
|
148
|
+
}
|
|
149
|
+
// 清理缓存函数
|
|
150
|
+
const cleanExpiredCache = async () => {
|
|
151
|
+
if (!config.enableCache || config.cacheExpiration <= 0)
|
|
152
|
+
return;
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const threshold = now - config.cacheExpiration * 60 * 60 * 1000;
|
|
155
|
+
// 清理解析缓存
|
|
156
|
+
await ctx.database.remove('sla_parse_cache', {
|
|
157
|
+
created_at: { $lt: threshold }
|
|
158
|
+
});
|
|
159
|
+
// 清理文件缓存
|
|
160
|
+
const expiredFiles = await ctx.database.get('sla_file_cache', {
|
|
161
|
+
created_at: { $lt: threshold }
|
|
162
|
+
});
|
|
163
|
+
for (const file of expiredFiles) {
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(file.path)) {
|
|
166
|
+
await fs.promises.unlink(file.path);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
logger.warn(`删除过期文件失败 ${file.path}: ${e}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await ctx.database.remove('sla_file_cache', {
|
|
174
|
+
created_at: { $lt: threshold }
|
|
175
|
+
});
|
|
176
|
+
if (expiredFiles.length > 0) {
|
|
177
|
+
logger.info(`已自动清理 ${expiredFiles.length} 个过期文件。`);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
// 设置定时清理
|
|
181
|
+
if (config.enableCache && config.autoCleanInterval > 0) {
|
|
182
|
+
ctx.setInterval(cleanExpiredCache, config.autoCleanInterval * 60 * 60 * 1000);
|
|
183
|
+
}
|
|
94
184
|
// 注册指令
|
|
95
185
|
const cmd = ctx.command('share', '分享解析插件配置', { authority: 1 })
|
|
96
186
|
.action(async ({ session }) => {
|
|
@@ -115,12 +205,9 @@ function apply(ctx, config) {
|
|
|
115
205
|
if (!value)
|
|
116
206
|
return '请输入正确的模式';
|
|
117
207
|
const mode = value.trim().toLowerCase() === 'true';
|
|
118
|
-
// @ts-ignore
|
|
119
208
|
const data = await ctx.database.get('sla_group_settings', session.guildId);
|
|
120
|
-
// @ts-ignore
|
|
121
209
|
const final_parsers = { ...data[0]?.custom_parsers, ...{ [parser]: mode } };
|
|
122
210
|
const record = { guildId: session.guildId, custom_parsers: final_parsers };
|
|
123
|
-
// @ts-ignore
|
|
124
211
|
await ctx.database.upsert('sla_group_settings', [record]);
|
|
125
212
|
}
|
|
126
213
|
await session.execute('share');
|
|
@@ -134,7 +221,6 @@ function apply(ctx, config) {
|
|
|
134
221
|
if (value) {
|
|
135
222
|
const mode = value.trim().toLowerCase() === 'true';
|
|
136
223
|
const record = { guildId: session.guildId, nsfw_enabled: mode };
|
|
137
|
-
// @ts-ignore
|
|
138
224
|
await ctx.database.upsert('sla_group_settings', [record]);
|
|
139
225
|
}
|
|
140
226
|
await session.execute('share');
|
|
@@ -145,11 +231,25 @@ function apply(ctx, config) {
|
|
|
145
231
|
return '该指令只能在群组中使用。';
|
|
146
232
|
if (!await (0, utils_1.isUserAdmin)(session, session.userId))
|
|
147
233
|
return '权限不足';
|
|
148
|
-
// @ts-ignore
|
|
149
234
|
await ctx.database.remove('sla_group_settings', { guildId: session.guildId });
|
|
150
235
|
return '已重置为全局默认设置。';
|
|
151
236
|
});
|
|
152
|
-
|
|
237
|
+
// 清除缓存指令
|
|
238
|
+
cmd.subcommand('.clean', '清除所有缓存和文件', { authority: 3 })
|
|
239
|
+
.action(async ({ session }) => {
|
|
240
|
+
await ctx.database.remove('sla_parse_cache', {});
|
|
241
|
+
const allFiles = await ctx.database.get('sla_file_cache', {});
|
|
242
|
+
for (const file of allFiles) {
|
|
243
|
+
try {
|
|
244
|
+
if (fs.existsSync(file.path)) {
|
|
245
|
+
await fs.promises.unlink(file.path);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { }
|
|
249
|
+
}
|
|
250
|
+
await ctx.database.remove('sla_file_cache', {});
|
|
251
|
+
return '缓存及对应文件已清理。';
|
|
252
|
+
});
|
|
153
253
|
const lastProcessedUrls = {};
|
|
154
254
|
ctx.on('ready', async () => {
|
|
155
255
|
logger.info('插件已启动,执行插件初始化');
|
|
@@ -168,8 +268,7 @@ function apply(ctx, config) {
|
|
|
168
268
|
if (session.guildId) {
|
|
169
269
|
const settings = await (0, utils_1.getEffectiveSettings)(ctx, session.guildId, config);
|
|
170
270
|
if (!settings.parsers[link.platform]) {
|
|
171
|
-
|
|
172
|
-
ctx.logger('share-links-analysis').info(`根据策略,该链接已被阻止解析:平台:${link.platform},链接:${link.url}`);
|
|
271
|
+
logger.debug(`根据策略,该链接已被阻止解析:平台:${link.platform},链接:${link.url}`);
|
|
173
272
|
if (config.showError)
|
|
174
273
|
await session.send(`根据策略,该链接已被阻止解析:平台:${link.platform}`);
|
|
175
274
|
continue;
|
|
@@ -177,8 +276,7 @@ function apply(ctx, config) {
|
|
|
177
276
|
}
|
|
178
277
|
else {
|
|
179
278
|
if (!config.default_parsers[link.platform]) {
|
|
180
|
-
|
|
181
|
-
ctx.logger('share-links-analysis').info(`根据策略,该链接已被阻止解析:平台:${link.platform},链接:${link.url}`);
|
|
279
|
+
logger.debug(`根据策略,该链接已被阻止解析:平台:${link.platform},链接:${link.url}`);
|
|
182
280
|
if (config.showError)
|
|
183
281
|
await session.send(`根据策略,该链接已被阻止解析:平台:${link.platform}`);
|
|
184
282
|
continue;
|
|
@@ -192,36 +290,66 @@ function apply(ctx, config) {
|
|
|
192
290
|
if (!lastProcessedUrls[channelId])
|
|
193
291
|
lastProcessedUrls[channelId] = {};
|
|
194
292
|
if (now - (lastProcessedUrls[channelId][link.url] || 0) < config.Min_Interval * 1000) {
|
|
195
|
-
|
|
196
|
-
logger.info(`链接 ${link.url} 在冷却时间内,跳过处理。`);
|
|
293
|
+
logger.debug(`链接 ${link.url} 在冷却时间内,跳过处理。`);
|
|
197
294
|
continue;
|
|
198
295
|
}
|
|
199
296
|
if (config.waitTip_Switch) {
|
|
200
297
|
await session.send(config.waitTip_Switch);
|
|
201
298
|
}
|
|
202
|
-
|
|
299
|
+
// === 缓存逻辑 ===
|
|
300
|
+
let result = null;
|
|
301
|
+
const cacheKey = `${link.platform}:${link.id}`;
|
|
302
|
+
if (config.enableCache) {
|
|
303
|
+
const cached = await ctx.database.get('sla_parse_cache', cacheKey);
|
|
304
|
+
// 检查是否存在且未过期
|
|
305
|
+
if (cached.length > 0) {
|
|
306
|
+
const entry = cached[0];
|
|
307
|
+
const isExpired = config.cacheExpiration > 0 && (Date.now() - entry.created_at > config.cacheExpiration * 60 * 60 * 1000);
|
|
308
|
+
if (!isExpired) {
|
|
309
|
+
logger.debug(`使用缓存解析结果: ${cacheKey}`);
|
|
310
|
+
result = entry.data;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// 过期删除
|
|
314
|
+
await ctx.database.remove('sla_parse_cache', { key: cacheKey });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// 缓存未命中,执行解析
|
|
319
|
+
if (!result) {
|
|
320
|
+
result = await (0, core_1.processLink)(ctx, config, link, session);
|
|
321
|
+
// 写入缓存
|
|
322
|
+
if (result && config.enableCache) {
|
|
323
|
+
await ctx.database.upsert('sla_parse_cache', [{
|
|
324
|
+
key: cacheKey,
|
|
325
|
+
data: result,
|
|
326
|
+
created_at: Date.now()
|
|
327
|
+
}]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// === 缓存逻辑结束 ===
|
|
203
331
|
if (result) {
|
|
204
332
|
lastProcessedUrls[channelId][link.url] = now;
|
|
205
|
-
await sendResult(session, config, result, logger);
|
|
333
|
+
await sendResult(ctx, session, config, result, logger);
|
|
206
334
|
}
|
|
207
335
|
linkCount++;
|
|
208
336
|
}
|
|
209
337
|
});
|
|
210
338
|
}
|
|
211
|
-
async function sendResult(session, config, result, logger) {
|
|
339
|
+
async function sendResult(ctx, session, config, result, logger) {
|
|
212
340
|
if (!session.channel) {
|
|
213
|
-
await (0, utils_1.sendResult_plain)(session, config, result, logger);
|
|
341
|
+
await (0, utils_1.sendResult_plain)(ctx, session, config, result, logger);
|
|
214
342
|
return;
|
|
215
343
|
}
|
|
216
344
|
switch (config.useForward) {
|
|
217
345
|
case "plain":
|
|
218
|
-
await (0, utils_1.sendResult_plain)(session, config, result, logger);
|
|
346
|
+
await (0, utils_1.sendResult_plain)(ctx, session, config, result, logger);
|
|
219
347
|
return;
|
|
220
348
|
case 'forward':
|
|
221
|
-
await (0, utils_1.sendResult_forward)(session, config, result, logger, false);
|
|
349
|
+
await (0, utils_1.sendResult_forward)(ctx, session, config, result, logger, false);
|
|
222
350
|
return;
|
|
223
351
|
case "mixed":
|
|
224
|
-
await (0, utils_1.sendResult_forward)(session, config, result, logger, true);
|
|
352
|
+
await (0, utils_1.sendResult_forward)(ctx, session, config, result, logger, true);
|
|
225
353
|
return;
|
|
226
354
|
}
|
|
227
355
|
}
|
package/lib/parsers/bilibili.js
CHANGED
|
@@ -92,9 +92,7 @@ async function process(ctx, config, link, session) {
|
|
|
92
92
|
if (locationHeader) {
|
|
93
93
|
finalUrl = locationHeader;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
logger.debug(`解析短链接时发生网络错误或未找到跳转地址: ${e.message}`);
|
|
97
|
-
}
|
|
95
|
+
logger.debug(`解析短链接时发生网络错误或未找到跳转地址: ${e.message}`);
|
|
98
96
|
}
|
|
99
97
|
if (finalUrl && (finalUrl.includes('b23.tv') || finalUrl.includes('bilibili.com/'))) {
|
|
100
98
|
const urlObj = new URL(finalUrl);
|
|
@@ -122,8 +120,7 @@ async function process(ctx, config, link, session) {
|
|
|
122
120
|
}
|
|
123
121
|
}
|
|
124
122
|
if (finalUrl) {
|
|
125
|
-
|
|
126
|
-
logger.info(`短链接解析成功,指向: ${finalUrl}`);
|
|
123
|
+
logger.debug(`短链接解析成功,指向: ${finalUrl}`);
|
|
127
124
|
const matchedLinks = match(finalUrl);
|
|
128
125
|
if (matchedLinks.length > 0 && matchedLinks[0].type === 'video') {
|
|
129
126
|
videoId = matchedLinks[0].id;
|
|
@@ -143,8 +140,7 @@ async function process(ctx, config, link, session) {
|
|
|
143
140
|
logger.warn(`无法从链接 ${link.url} 中解析出有效的B站视频ID。`);
|
|
144
141
|
return null;
|
|
145
142
|
}
|
|
146
|
-
|
|
147
|
-
logger.info(`获取视频信息,ID: ${videoId}`);
|
|
143
|
+
logger.debug(`获取视频信息,ID: ${videoId}`);
|
|
148
144
|
const idType = videoId.startsWith('BV') ? 'bvid' : 'aid';
|
|
149
145
|
const infoUrl = `https://api.bilibili.com/x/web-interface/view?${idType}=${videoId}`;
|
|
150
146
|
try {
|
|
@@ -158,14 +154,12 @@ async function process(ctx, config, link, session) {
|
|
|
158
154
|
const data = info.data;
|
|
159
155
|
// --- 获取视频直链 ---
|
|
160
156
|
let videoUrl = null;
|
|
161
|
-
|
|
162
|
-
logger.info(`尝试获取视频流,bvid: ${data.bvid}`);
|
|
157
|
+
logger.debug(`尝试获取视频流,bvid: ${data.bvid}`);
|
|
163
158
|
try {
|
|
164
159
|
const videoStream = await ctx.BiliBiliVideo.getBilibiliVideoStream(data.aid, data.bvid, data.pages[0].cid, config.Video_ClarityPriority === '1' ? 32 : 80, 'html5', 1);
|
|
165
160
|
if (videoStream?.data?.durl?.[0]?.url) {
|
|
166
161
|
videoUrl = videoStream.data.durl[0].url;
|
|
167
|
-
|
|
168
|
-
logger.info(`成功获取视频流,bvid: ${data.bvid}`);
|
|
162
|
+
logger.debug(`成功获取视频流,bvid: ${data.bvid}`);
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
catch (e) {
|
package/lib/parsers/twitter.js
CHANGED
|
@@ -113,8 +113,7 @@ async function init(ctx, config) {
|
|
|
113
113
|
const filteredCookies = finalCookies.filter((c) => c.name !== 'acw_tc');
|
|
114
114
|
// 使用过滤后的 cookie 数组来生成字符串
|
|
115
115
|
const cookieString = filteredCookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
116
|
-
|
|
117
|
-
await ctx.database.upsert('sla_cookie_cache', [{ platform: platformId, cookie: cookieString }]);
|
|
116
|
+
await ctx.database.upsert('sla_cookie_cache', [{ platform: exports.name, cookie: cookieString }]);
|
|
118
117
|
logger.info('成功执行两步刷新策略并缓存了小红书 Cookie!');
|
|
119
118
|
return true;
|
|
120
119
|
}
|
|
@@ -145,22 +144,20 @@ async function process(ctx, config, link, session) {
|
|
|
145
144
|
const decodedUrl = link.url.replace(/&/g, '&');
|
|
146
145
|
const originalUrl = new URL(decodedUrl);
|
|
147
146
|
token = originalUrl.searchParams.get('xsec_token');
|
|
148
|
-
if (token
|
|
149
|
-
logger.
|
|
147
|
+
if (token) {
|
|
148
|
+
logger.debug(`成功从分享链接中提取 xsec_token。`);
|
|
150
149
|
}
|
|
151
|
-
else
|
|
150
|
+
else {
|
|
152
151
|
logger.debug(`分享链接中未找到 xsec_token: ${link.url}`);
|
|
153
152
|
}
|
|
154
153
|
}
|
|
155
154
|
catch (e) {
|
|
156
|
-
|
|
157
|
-
logger.debug(`解析分享链接URL失败: ${link.url}`);
|
|
155
|
+
logger.debug(`解析分享链接URL失败: ${link.url}`);
|
|
158
156
|
}
|
|
159
157
|
let finalUrl = link.url;
|
|
160
158
|
// 步骤二:如果是短链接,获取其跳转后的基础地址
|
|
161
159
|
if (link.url.includes('xhslink.com')) {
|
|
162
|
-
|
|
163
|
-
logger.info(`小红书短链接解析:尝试获取 ${link.url} 的最终地址`);
|
|
160
|
+
logger.debug(`小红书短链接解析:尝试获取 ${link.url} 的最终地址`);
|
|
164
161
|
try {
|
|
165
162
|
await ctx.http(link.url, {
|
|
166
163
|
method: 'GET',
|
|
@@ -172,8 +169,7 @@ async function process(ctx, config, link, session) {
|
|
|
172
169
|
const location = e.response?.headers?.location;
|
|
173
170
|
if (location) {
|
|
174
171
|
finalUrl = location;
|
|
175
|
-
|
|
176
|
-
logger.info(`短链接解析成功,跳转地址: ${finalUrl}`);
|
|
172
|
+
logger.debug(`短链接解析成功,跳转地址: ${finalUrl}`);
|
|
177
173
|
}
|
|
178
174
|
else {
|
|
179
175
|
logger.error(`解析短链接时发生网络错误: ${e.message}`);
|
|
@@ -198,12 +194,9 @@ async function process(ctx, config, link, session) {
|
|
|
198
194
|
logger.error(`构建最终请求URL失败: ${finalUrl}`);
|
|
199
195
|
return null;
|
|
200
196
|
}
|
|
201
|
-
|
|
202
|
-
logger.info(`正在抓取小红书页面: ${urlToFetch}`);
|
|
197
|
+
logger.debug(`正在抓取小红书页面: ${urlToFetch}`);
|
|
203
198
|
try {
|
|
204
|
-
|
|
205
|
-
const dbCache = await ctx.database.get('sla_cookie_cache', platformId);
|
|
206
|
-
// @ts-ignore
|
|
199
|
+
const dbCache = await ctx.database.get('sla_cookie_cache', exports.name);
|
|
207
200
|
let currentCookie = (dbCache && dbCache.length > 0) ? dbCache[0].cookie : '';
|
|
208
201
|
const requestHeaders = {
|
|
209
202
|
'User-Agent': config.userAgent,
|
|
@@ -236,14 +229,10 @@ async function process(ctx, config, link, session) {
|
|
|
236
229
|
let coverUrl = undefined;
|
|
237
230
|
const images = [];
|
|
238
231
|
if (noteData.type === 'video' && noteData.video) {
|
|
239
|
-
|
|
240
|
-
logger.info(`[XHS Video Debug] 发现视频笔记,视频数据对象: \n${JSON.stringify(noteData.video, null, 2)}`);
|
|
241
|
-
}
|
|
232
|
+
logger.debug(`[XHS Video Debug] 发现视频笔记,视频数据对象: \n${JSON.stringify(noteData.video, null, 2)}`);
|
|
242
233
|
if (noteData.video.media?.stream?.h264?.[0]?.masterUrl) {
|
|
243
234
|
videoUrl = noteData.video.media.stream.h264[0].masterUrl;
|
|
244
|
-
|
|
245
|
-
logger.info(`[XHS Video Debug] 已提取视频链接: ${videoUrl}`);
|
|
246
|
-
}
|
|
235
|
+
logger.debug(`[XHS Video Debug] 已提取视频链接: ${videoUrl}`);
|
|
247
236
|
}
|
|
248
237
|
else {
|
|
249
238
|
logger.warn('[XHS Video Debug] 未能从预期路径 `note.video.media.stream.h264[0].masterUrl` 找到视频链接。');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context, Session } from 'koishi';
|
|
2
|
+
import { Link, ParsedInfo, PluginConfig } from '../types';
|
|
3
|
+
export declare const name = "youtube";
|
|
4
|
+
export declare function match(content: string): Link[];
|
|
5
|
+
/**
|
|
6
|
+
* 初始化:尝试获取 Cookie
|
|
7
|
+
* 策略:优先 Puppeteer (最稳),失败则回退到 HTTP 请求 (利用插件自身的代理设置)
|
|
8
|
+
*/
|
|
9
|
+
export declare function init(ctx: Context, config: PluginConfig): Promise<boolean>;
|
|
10
|
+
export declare function process(ctx: Context, config: PluginConfig, link: Link, session: Session): Promise<ParsedInfo | null>;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/parsers/youtube.ts
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.name = void 0;
|
|
8
|
+
exports.match = match;
|
|
9
|
+
exports.init = init;
|
|
10
|
+
exports.process = process;
|
|
11
|
+
const utils_1 = require("../utils");
|
|
12
|
+
const ytdl_core_1 = __importDefault(require("@distube/ytdl-core"));
|
|
13
|
+
exports.name = "youtube";
|
|
14
|
+
// 匹配规则:支持普通视频、短链接 (youtu.be)、Shorts 以及 Embed 链接
|
|
15
|
+
const linkRules = [
|
|
16
|
+
{
|
|
17
|
+
pattern: /https?:\/\/(?:www\.|m\.)?youtube\.com\/watch\?v=([\w-]{11})/gi,
|
|
18
|
+
type: "video",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /https?:\/\/youtu\.be\/([\w-]{11})/gi,
|
|
22
|
+
type: "video",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /https?:\/\/(?:www\.|m\.)?youtube\.com\/shorts\/([\w-]{11})/gi,
|
|
26
|
+
type: "shorts",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /https?:\/\/(?:www\.|m\.)?youtube\.com\/(?:v|embed)\/([\w-]{11})/gi,
|
|
30
|
+
type: "video",
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
function match(content) {
|
|
34
|
+
const results = [];
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
for (const rule of linkRules) {
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = rule.pattern.exec(content)) !== null) {
|
|
39
|
+
const id = match[1];
|
|
40
|
+
const url = `https://www.youtube.com/watch?v=${id}`;
|
|
41
|
+
if (seen.has(url))
|
|
42
|
+
continue;
|
|
43
|
+
seen.add(url);
|
|
44
|
+
results.push({
|
|
45
|
+
platform: exports.name,
|
|
46
|
+
type: rule.type,
|
|
47
|
+
id,
|
|
48
|
+
url,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 初始化:尝试获取 Cookie
|
|
56
|
+
* 策略:优先 Puppeteer (最稳),失败则回退到 HTTP 请求 (利用插件自身的代理设置)
|
|
57
|
+
*/
|
|
58
|
+
async function init(ctx, config) {
|
|
59
|
+
const logger = ctx.logger('share-links-analysis:youtube');
|
|
60
|
+
// 1. 检查是否已配置手动 Cookie
|
|
61
|
+
if (config.youtubeCookie && config.youtubeCookie.trim().length > 0) {
|
|
62
|
+
logger.info('检测到配置文件中已填写手动 Cookie,将优先使用该 Cookie,跳过自动刷新。');
|
|
63
|
+
// 可选:将配置的 Cookie 同步到数据库,或者在 process 中直接读取配置
|
|
64
|
+
// 这里我们选择不写入数据库,而是每次 process 时直接读配置,方便用户随时修改
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
// 2. 自动获取逻辑 (仅当未配置手动 Cookie 时执行)
|
|
68
|
+
let cookieString = '';
|
|
69
|
+
// --- 策略 A: 尝试 Puppeteer (受全局 puppeteer 配置影响) ---
|
|
70
|
+
if (ctx.puppeteer) {
|
|
71
|
+
logger.info('尝试通过 Puppeteer 获取 Cookie...');
|
|
72
|
+
let page = null;
|
|
73
|
+
try {
|
|
74
|
+
page = await ctx.puppeteer.page();
|
|
75
|
+
await page.setUserAgent(config.userAgent);
|
|
76
|
+
// Puppeteer 只能走全局代理或 Chrome 参数代理,无法在这里动态设置
|
|
77
|
+
await page.goto('https://www.youtube.com', {
|
|
78
|
+
waitUntil: 'domcontentloaded',
|
|
79
|
+
timeout: 15000
|
|
80
|
+
});
|
|
81
|
+
// 尝试点击同意 (针对欧盟IP)
|
|
82
|
+
try {
|
|
83
|
+
const consentButton = await page.$('button[aria-label*="Accept"]');
|
|
84
|
+
if (consentButton)
|
|
85
|
+
await consentButton.click();
|
|
86
|
+
}
|
|
87
|
+
catch (e) { }
|
|
88
|
+
const cookies = await page.cookies();
|
|
89
|
+
if (cookies.length > 0) {
|
|
90
|
+
cookieString = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
91
|
+
logger.info(`Puppeteer 成功: 获取到 ${cookies.length} 个 Cookie`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
logger.warn(`Puppeteer 获取失败: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (page)
|
|
99
|
+
await page.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// --- 策略 B: 如果 Puppeteer 失败,尝试 HTTP 回退 (使用插件独立代理) ---
|
|
103
|
+
if (!cookieString) {
|
|
104
|
+
logger.info('尝试通过 HTTP 请求回退获取 Cookie...');
|
|
105
|
+
try {
|
|
106
|
+
// 构造请求头
|
|
107
|
+
const headers = {
|
|
108
|
+
'User-Agent': config.userAgent,
|
|
109
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
110
|
+
};
|
|
111
|
+
// 创建一个专用的 http 实例,显式应用插件配置的 proxy
|
|
112
|
+
// 这样才能确保 request 走你在插件里填写的代理,而不是 Koishi 全局代理
|
|
113
|
+
const http = config.proxy
|
|
114
|
+
? ctx.http.extend({ proxy: config.proxy })
|
|
115
|
+
: ctx.http;
|
|
116
|
+
const res = await http('https://www.youtube.com', {
|
|
117
|
+
method: 'HEAD',
|
|
118
|
+
headers: headers,
|
|
119
|
+
redirect: 'manual'
|
|
120
|
+
});
|
|
121
|
+
let setCookie = [];
|
|
122
|
+
// 兼容处理 Headers: 标准 API (Node 18+) 或 Polyfill
|
|
123
|
+
if (res.headers && typeof res.headers.getSetCookie === 'function') {
|
|
124
|
+
setCookie = res.headers.getSetCookie();
|
|
125
|
+
}
|
|
126
|
+
else if (res.headers && typeof res.headers.get === 'function') {
|
|
127
|
+
// 降级:部分环境可能没有 getSetCookie
|
|
128
|
+
const raw = res.headers.get('set-cookie');
|
|
129
|
+
if (raw)
|
|
130
|
+
setCookie = [raw];
|
|
131
|
+
}
|
|
132
|
+
else if (res.headers && Array.isArray(res.headers['set-cookie'])) {
|
|
133
|
+
// 兼容旧版 axios 风格返回
|
|
134
|
+
setCookie = res.headers['set-cookie'];
|
|
135
|
+
}
|
|
136
|
+
if (setCookie && setCookie.length > 0) {
|
|
137
|
+
cookieString = setCookie.map(str => str.split(';')[0]).join('; ');
|
|
138
|
+
logger.info(`HTTP 回退成功: 获取到 Cookie 字符串`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
logger.warn('HTTP 请求未返回 Set-Cookie 头');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
logger.warn(`HTTP 回退失败: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// --- 写入数据库 ---
|
|
149
|
+
if (cookieString) {
|
|
150
|
+
await ctx.database.upsert('sla_cookie_cache', [{ platform: exports.name, cookie: cookieString }]);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
logger.error('所有策略均失败,无法获取 YouTube Cookie。');
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 辅助函数:解析 Cookie 字符串
|
|
159
|
+
function parseCookieString(cookieString) {
|
|
160
|
+
if (!cookieString)
|
|
161
|
+
return [];
|
|
162
|
+
return cookieString.split(';').map(pair => {
|
|
163
|
+
const parts = pair.split('=');
|
|
164
|
+
// 处理 value 中可能包含 = 的情况
|
|
165
|
+
const name = parts.shift();
|
|
166
|
+
const value = parts.join('=');
|
|
167
|
+
if (name && value)
|
|
168
|
+
return { name: name.trim(), value: value.trim() };
|
|
169
|
+
return null;
|
|
170
|
+
}).filter((c) => c !== null);
|
|
171
|
+
}
|
|
172
|
+
async function process(ctx, config, link, session) {
|
|
173
|
+
const logger = ctx.logger(`share-links-analysis:${exports.name}`);
|
|
174
|
+
const videoUrl = `https://www.youtube.com/watch?v=${link.id}`;
|
|
175
|
+
// 修复 2: 提升变量作用域,确保 catch 块能访问
|
|
176
|
+
let cookieString = '';
|
|
177
|
+
try {
|
|
178
|
+
// 1. 准备 Cookie
|
|
179
|
+
// 优先级:配置的手动 Cookie > 数据库缓存的自动 Cookie
|
|
180
|
+
if (config.youtubeCookie && config.youtubeCookie.trim().length > 0) {
|
|
181
|
+
cookieString = config.youtubeCookie.trim();
|
|
182
|
+
logger.debug('使用配置文件中的手动 Cookie。');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const dbCache = await ctx.database.get('sla_cookie_cache', exports.name);
|
|
186
|
+
cookieString = (dbCache && dbCache.length > 0) ? dbCache[0].cookie : '';
|
|
187
|
+
if (cookieString)
|
|
188
|
+
logger.debug('使用数据库缓存的自动 Cookie。');
|
|
189
|
+
}
|
|
190
|
+
if (!cookieString) {
|
|
191
|
+
logger.debug('无 Cookie,将尝试裸连解析(风险较高)。');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
logger.debug('已加载 Cookie,准备解析。');
|
|
195
|
+
}
|
|
196
|
+
const cookies = parseCookieString(cookieString);
|
|
197
|
+
// 2. 创建 Agent (ytdl 专用)
|
|
198
|
+
// 无论 Puppeteer 是否有代理,这里必须使用插件配置的代理来请求视频流
|
|
199
|
+
let agent;
|
|
200
|
+
if (config.proxy) {
|
|
201
|
+
// 使用 createProxyAgent 同时处理 代理 和 Cookie
|
|
202
|
+
// 注意:@distube/ytdl-core 4.x+ 支持此方法
|
|
203
|
+
if (typeof ytdl_core_1.default.createProxyAgent === 'function') {
|
|
204
|
+
logger.debug(`使用代理: ${config.proxy}`);
|
|
205
|
+
agent = ytdl_core_1.default.createProxyAgent({ uri: config.proxy }, cookies);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// 如果版本较旧没有 createProxyAgent,回退到普通 Agent (代理会失效)
|
|
209
|
+
logger.warn('ytdl.createProxyAgent 不存在,将忽略代理设置。请更新 @distube/ytdl-core');
|
|
210
|
+
agent = ytdl_core_1.default.createAgent(cookies);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// 无代理,直接使用 createAgent
|
|
215
|
+
agent = ytdl_core_1.default.createAgent(cookies);
|
|
216
|
+
}
|
|
217
|
+
// 3. 获取视频信息
|
|
218
|
+
logger.debug(`正在解析 YouTube 视频: ${link.id}`);
|
|
219
|
+
// 尝试获取信息
|
|
220
|
+
// 为了提高成功率,我们允许 ytdl 尝试使用不同的内置客户端 (如 Android, iOS)
|
|
221
|
+
// 注意:@distube/ytdl-core 会自动处理客户端回退,但我们可以显式传参
|
|
222
|
+
const info = await ytdl_core_1.default.getInfo(videoUrl, {
|
|
223
|
+
agent, // 将正确的 Agent 传递给顶层选项
|
|
224
|
+
requestOptions: {
|
|
225
|
+
headers: {
|
|
226
|
+
'User-Agent': config.userAgent,
|
|
227
|
+
// 确保 headers 里也带上 Cookie,增加成功率
|
|
228
|
+
'Cookie': cookieString
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const details = info.videoDetails;
|
|
233
|
+
// 4. 提取元数据
|
|
234
|
+
const title = details.title;
|
|
235
|
+
const authorName = details.author.name;
|
|
236
|
+
const description = details.description || '';
|
|
237
|
+
// 获取最高分辨率的封面
|
|
238
|
+
const thumbnails = details.thumbnails;
|
|
239
|
+
const coverUrl = thumbnails.length > 0 ? thumbnails[thumbnails.length - 1].url : undefined;
|
|
240
|
+
const views = (0, utils_1.numeral)(parseInt(details.viewCount), config);
|
|
241
|
+
const likes = details.likes ? (0, utils_1.numeral)(details.likes, config) : '未知';
|
|
242
|
+
const statsString = `观看: ${views} | 点赞: ${likes}`;
|
|
243
|
+
// 5. 选择最佳视频流
|
|
244
|
+
// 优先寻找 MP4 封装且包含音视频的格式 (兼容性最好)
|
|
245
|
+
let format = ytdl_core_1.default.chooseFormat(info.formats, {
|
|
246
|
+
quality: 'highest',
|
|
247
|
+
filter: (f) => f.container === 'mp4' && f.hasAudio && f.hasVideo
|
|
248
|
+
});
|
|
249
|
+
// 如果找不到 MP4 合流,尝试任意音视频合流
|
|
250
|
+
if (!format) {
|
|
251
|
+
logger.debug('未找到 MP4 合流格式,尝试查找任意音视频合流...');
|
|
252
|
+
try {
|
|
253
|
+
format = ytdl_core_1.default.chooseFormat(info.formats, {
|
|
254
|
+
quality: 'highest',
|
|
255
|
+
filter: 'audioandvideo'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (e) { }
|
|
259
|
+
}
|
|
260
|
+
// 6. 构建文件列表
|
|
261
|
+
const files = [];
|
|
262
|
+
if (format && format.url) {
|
|
263
|
+
// 只有当存在有效链接时才添加
|
|
264
|
+
files.push({ type: 'video', url: format.url });
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
logger.warn('未找到合适的视频流格式。');
|
|
268
|
+
}
|
|
269
|
+
// 7. 构建正文
|
|
270
|
+
const mainbody = (0, utils_1.escapeHtml)(description);
|
|
271
|
+
return {
|
|
272
|
+
platform: exports.name,
|
|
273
|
+
title: title,
|
|
274
|
+
authorName: authorName,
|
|
275
|
+
mainbody: mainbody,
|
|
276
|
+
coverUrl: coverUrl,
|
|
277
|
+
files: files,
|
|
278
|
+
sourceUrl: videoUrl,
|
|
279
|
+
stats: statsString,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (error.message.includes('Sign in') || error.message.includes('429')) {
|
|
284
|
+
const tip = cookieString
|
|
285
|
+
? '当前 Cookie 可能已失效,请更新配置中的 Cookie。'
|
|
286
|
+
: '请在插件配置中填入已登录账号的 YouTube Cookie。';
|
|
287
|
+
await session.send(`YouTube 解析被拦截:${tip}`);
|
|
288
|
+
logger.warn(`解析拦截: ${error.message}`);
|
|
289
|
+
}
|
|
290
|
+
else if (error.message.includes('Private video')) {
|
|
291
|
+
await session.send('解析失败:私享视频。');
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
logger.error(`解析异常: ${error.message}`);
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
package/lib/types.d.ts
CHANGED
|
@@ -27,18 +27,22 @@ export interface PluginConfig {
|
|
|
27
27
|
usingLocal: boolean;
|
|
28
28
|
sendFiles: boolean;
|
|
29
29
|
sendLinks: boolean;
|
|
30
|
+
enableCache: boolean;
|
|
31
|
+
cacheExpiration: number;
|
|
32
|
+
autoCleanInterval: number;
|
|
30
33
|
format: string;
|
|
31
34
|
parseLimit: number;
|
|
32
35
|
useNumeral: boolean;
|
|
33
36
|
showError: boolean;
|
|
37
|
+
youtubeCookie?: string;
|
|
34
38
|
proxy: string;
|
|
35
|
-
proxy_settings:
|
|
36
|
-
default_parsers:
|
|
39
|
+
proxy_settings: Record<string, boolean>;
|
|
40
|
+
default_parsers: Record<string, boolean>;
|
|
37
41
|
allow_sensitive: boolean;
|
|
38
42
|
onebotReadDir: string;
|
|
39
43
|
localDownloadDir: string;
|
|
40
44
|
userAgent: string;
|
|
41
|
-
|
|
45
|
+
debug: boolean;
|
|
42
46
|
}
|
|
43
47
|
export interface BilibiliVideoInfo {
|
|
44
48
|
data: {
|
|
@@ -79,6 +83,32 @@ declare module 'koishi' {
|
|
|
79
83
|
BiliBiliVideo: any;
|
|
80
84
|
puppeteer?: any;
|
|
81
85
|
}
|
|
86
|
+
interface Tables {
|
|
87
|
+
sla_parse_cache: SlaParseCache;
|
|
88
|
+
sla_file_cache: SlaFileCache;
|
|
89
|
+
sla_cookie_cache: SlaCookieCache;
|
|
90
|
+
sla_group_settings: SlaGroupSettings;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export interface SlaParseCache {
|
|
94
|
+
key: string;
|
|
95
|
+
data: ParsedInfo;
|
|
96
|
+
created_at: number;
|
|
97
|
+
}
|
|
98
|
+
export interface SlaFileCache {
|
|
99
|
+
hash: string;
|
|
100
|
+
path: string;
|
|
101
|
+
url: string;
|
|
102
|
+
created_at: number;
|
|
103
|
+
}
|
|
104
|
+
export interface SlaCookieCache {
|
|
105
|
+
platform: string;
|
|
106
|
+
cookie: string;
|
|
107
|
+
}
|
|
108
|
+
export interface SlaGroupSettings {
|
|
109
|
+
guildId: string;
|
|
110
|
+
custom_parsers: Record<string, boolean>;
|
|
111
|
+
nsfw_enabled: boolean;
|
|
82
112
|
}
|
|
83
113
|
export interface XhsImageInfo {
|
|
84
114
|
imageScene: string;
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ParsedInfo, PluginConfig } from './types';
|
|
2
2
|
import { Context, Logger, Session } from "koishi";
|
|
3
|
+
import { Agent as HttpAgent } from 'http';
|
|
4
|
+
import { Agent as HttpsAgent } from 'https';
|
|
5
|
+
import 'koishi-plugin-adapter-onebot';
|
|
3
6
|
/**
|
|
4
7
|
* 将数字格式化为易读的字符串(如 万、亿)
|
|
5
8
|
* @param num 数字
|
|
@@ -9,11 +12,12 @@ import { Context, Logger, Session } from "koishi";
|
|
|
9
12
|
export declare function numeral(num: number, config: PluginConfig): string;
|
|
10
13
|
export declare function escapeHtml(str: string): string;
|
|
11
14
|
export declare function unescapeHtml(str: string): string;
|
|
15
|
+
export declare function getProxyAgent(proxy: string | undefined, url: string): HttpAgent | HttpsAgent | undefined;
|
|
12
16
|
export declare function getFileSize(url: string, proxy: string | undefined, userAgent: string | undefined, logger: Logger): Promise<number | null>;
|
|
13
17
|
export declare function getEffectiveSettings(ctx: Context, guildId: string | undefined, config: PluginConfig): Promise<{
|
|
14
|
-
parsers:
|
|
15
|
-
nsfw:
|
|
18
|
+
parsers: Record<string, boolean>;
|
|
19
|
+
nsfw: boolean;
|
|
16
20
|
}>;
|
|
17
21
|
export declare function isUserAdmin(session: Session, userId: string): Promise<boolean>;
|
|
18
|
-
export declare function sendResult_plain(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger): Promise<void>;
|
|
19
|
-
export declare function sendResult_forward(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger, mixed_sending?: boolean): Promise<void>;
|
|
22
|
+
export declare function sendResult_plain(ctx: Context, session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger): Promise<void>;
|
|
23
|
+
export declare function sendResult_forward(ctx: Context, session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger, mixed_sending?: boolean): Promise<void>;
|
package/lib/utils.js
CHANGED
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.numeral = numeral;
|
|
40
40
|
exports.escapeHtml = escapeHtml;
|
|
41
41
|
exports.unescapeHtml = unescapeHtml;
|
|
42
|
+
exports.getProxyAgent = getProxyAgent;
|
|
42
43
|
exports.getFileSize = getFileSize;
|
|
43
44
|
exports.getEffectiveSettings = getEffectiveSettings;
|
|
44
45
|
exports.isUserAdmin = isUserAdmin;
|
|
@@ -53,6 +54,8 @@ const url_1 = require("url");
|
|
|
53
54
|
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
54
55
|
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
55
56
|
const fs = __importStar(require("node:fs"));
|
|
57
|
+
const crypto_1 = require("crypto");
|
|
58
|
+
require("koishi-plugin-adapter-onebot");
|
|
56
59
|
/**
|
|
57
60
|
* 将数字格式化为易读的字符串(如 万、亿)
|
|
58
61
|
* @param num 数字
|
|
@@ -122,14 +125,50 @@ function parseHtmlToSegments(html) {
|
|
|
122
125
|
}
|
|
123
126
|
return segments;
|
|
124
127
|
}
|
|
125
|
-
async function downloadAndMapUrl(url, proxy, userAgent, localDownloadDir, onebotReadDir, logger) {
|
|
128
|
+
async function downloadAndMapUrl(ctx, url, proxy, userAgent, localDownloadDir, onebotReadDir, logger, enableCache) {
|
|
126
129
|
await fs.promises.mkdir(localDownloadDir, { recursive: true });
|
|
130
|
+
// 1. 计算 Hash
|
|
131
|
+
const hash = (0, crypto_1.createHash)('md5').update(url).digest('hex');
|
|
127
132
|
const u = new url_1.URL(url);
|
|
128
133
|
const ext = path_1.default.extname(u.pathname).split('?')[0] || '.bin';
|
|
129
|
-
|
|
134
|
+
// 如果开启缓存,先查库
|
|
135
|
+
if (enableCache) {
|
|
136
|
+
try {
|
|
137
|
+
const cached = await ctx.database.get('sla_file_cache', hash);
|
|
138
|
+
if (cached.length > 0) {
|
|
139
|
+
const cachedPath = cached[0].path;
|
|
140
|
+
if (fs.existsSync(cachedPath)) {
|
|
141
|
+
const filename = path_1.default.basename(cachedPath);
|
|
142
|
+
const onebotPath = path_1.default.posix.join(onebotReadDir, filename);
|
|
143
|
+
logger.debug(`缓存命中: ${url} -> ${cachedPath}`);
|
|
144
|
+
return `file://${onebotPath}`;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// 数据库有记录但文件不存在,删除记录
|
|
148
|
+
await ctx.database.remove('sla_file_cache', { hash });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
logger.warn(`读取文件缓存失败,将重新下载: ${e}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 2. 生成文件名 (使用 Hash 以实现去重)
|
|
157
|
+
const safeFilename = `${hash}${ext}`;
|
|
130
158
|
const actualPath = path_1.default.join(localDownloadDir, safeFilename);
|
|
131
159
|
const onebotPath = path_1.default.posix.join(onebotReadDir, safeFilename);
|
|
132
160
|
const fileUrl = `file://${onebotPath}`;
|
|
161
|
+
// 3. 检查本地文件是否存在 (双重保险,或者应对未清理的情况)
|
|
162
|
+
if (enableCache && fs.existsSync(actualPath)) {
|
|
163
|
+
// 补写数据库
|
|
164
|
+
await ctx.database.upsert('sla_file_cache', [{
|
|
165
|
+
hash,
|
|
166
|
+
path: actualPath,
|
|
167
|
+
url,
|
|
168
|
+
created_at: Date.now()
|
|
169
|
+
}]);
|
|
170
|
+
return fileUrl;
|
|
171
|
+
}
|
|
133
172
|
return new Promise((resolve, reject) => {
|
|
134
173
|
const agent = getProxyAgent(proxy, url);
|
|
135
174
|
const headers = {
|
|
@@ -144,7 +183,7 @@ async function downloadAndMapUrl(url, proxy, userAgent, localDownloadDir, onebot
|
|
|
144
183
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
145
184
|
req.destroy();
|
|
146
185
|
logger.debug(`重定向: ${url} -> ${res.headers.location}`);
|
|
147
|
-
downloadAndMapUrl(res.headers.location, proxy, userAgent, localDownloadDir, onebotReadDir, logger)
|
|
186
|
+
downloadAndMapUrl(ctx, res.headers.location, proxy, userAgent, localDownloadDir, onebotReadDir, logger, enableCache)
|
|
148
187
|
.then(resolve)
|
|
149
188
|
.catch(reject);
|
|
150
189
|
return;
|
|
@@ -163,8 +202,22 @@ async function downloadAndMapUrl(url, proxy, userAgent, localDownloadDir, onebot
|
|
|
163
202
|
}
|
|
164
203
|
const pipelineAsync = (0, util_1.promisify)(stream_1.pipeline);
|
|
165
204
|
pipelineAsync(res, (0, fs_1.createWriteStream)(actualPath))
|
|
166
|
-
.then(() => {
|
|
205
|
+
.then(async () => {
|
|
167
206
|
logger.debug(`下载成功: ${url} -> ${fileUrl}`);
|
|
207
|
+
// 下载成功,写入数据库缓存
|
|
208
|
+
if (enableCache) {
|
|
209
|
+
try {
|
|
210
|
+
await ctx.database.upsert('sla_file_cache', [{
|
|
211
|
+
hash,
|
|
212
|
+
path: actualPath,
|
|
213
|
+
url,
|
|
214
|
+
created_at: Date.now()
|
|
215
|
+
}]);
|
|
216
|
+
}
|
|
217
|
+
catch (dbErr) {
|
|
218
|
+
logger.warn(`写入文件缓存数据库失败: ${dbErr}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
168
221
|
resolve(fileUrl);
|
|
169
222
|
})
|
|
170
223
|
.catch((err) => {
|
|
@@ -287,13 +340,10 @@ async function getEffectiveSettings(ctx, guildId, config) {
|
|
|
287
340
|
nsfw: config.allow_sensitive
|
|
288
341
|
};
|
|
289
342
|
}
|
|
290
|
-
// @ts-ignore
|
|
291
343
|
const data = await ctx.database.get('sla_group_settings', guildId);
|
|
292
344
|
const record = data[0];
|
|
293
345
|
// 合并:自定义设置覆盖默认
|
|
294
|
-
// @ts-ignore
|
|
295
346
|
const effectiveParsers = { ...config.default_parsers, ...record?.custom_parsers ? record.custom_parsers : {} };
|
|
296
|
-
// @ts-ignore
|
|
297
347
|
const nsfw_enabled = record?.nsfw_enabled ? record.nsfw_enabled : config.allow_sensitive;
|
|
298
348
|
return {
|
|
299
349
|
parsers: effectiveParsers,
|
|
@@ -303,7 +353,7 @@ async function getEffectiveSettings(ctx, guildId, config) {
|
|
|
303
353
|
async function isUserAdmin(session, userId) {
|
|
304
354
|
if (!session.guildId)
|
|
305
355
|
return false;
|
|
306
|
-
//
|
|
356
|
+
// 使用 (session.user as any) 来规避类型检查,同时保留可选链以防 user 为空
|
|
307
357
|
if (session.user?.authority >= 3)
|
|
308
358
|
return true;
|
|
309
359
|
try {
|
|
@@ -322,10 +372,8 @@ async function isUserAdmin(session, userId) {
|
|
|
322
372
|
return true;
|
|
323
373
|
}
|
|
324
374
|
}
|
|
325
|
-
async function sendResult_plain(session, config, result, logger) {
|
|
326
|
-
|
|
327
|
-
logger.info('进入普通发送');
|
|
328
|
-
}
|
|
375
|
+
async function sendResult_plain(ctx, session, config, result, logger) {
|
|
376
|
+
logger.debug('进入普通发送');
|
|
329
377
|
const localDownloadDir = config.localDownloadDir;
|
|
330
378
|
const onebotReadDir = config.onebotReadDir;
|
|
331
379
|
let mediaCoverUrl = result.coverUrl;
|
|
@@ -333,15 +381,14 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
333
381
|
let proxy = undefined;
|
|
334
382
|
if (config.proxy_settings[result.platform]) {
|
|
335
383
|
proxy = config.proxy;
|
|
336
|
-
logger.
|
|
384
|
+
logger.debug("正在使用代理");
|
|
337
385
|
}
|
|
338
386
|
// --- 下载封面 ---
|
|
339
387
|
if (result.coverUrl) {
|
|
340
388
|
if (config.usingLocal) {
|
|
341
389
|
try {
|
|
342
|
-
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
343
|
-
|
|
344
|
-
logger.info(`封面已下载: ${mediaCoverUrl}`);
|
|
390
|
+
mediaCoverUrl = await downloadAndMapUrl(ctx, result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger, config.enableCache);
|
|
391
|
+
logger.debug(`封面已下载: ${mediaCoverUrl}`);
|
|
345
392
|
}
|
|
346
393
|
catch (e) {
|
|
347
394
|
logger.warn(`封面下载失败: ${result.coverUrl}`, e);
|
|
@@ -360,10 +407,9 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
360
407
|
const remoteUrl = match[1];
|
|
361
408
|
if (config.usingLocal) {
|
|
362
409
|
try {
|
|
363
|
-
const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
410
|
+
const localUrl = await downloadAndMapUrl(ctx, remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger, config.enableCache);
|
|
364
411
|
urlMap[remoteUrl] = localUrl;
|
|
365
|
-
|
|
366
|
-
logger.info(`正文图片已下载: ${localUrl}`);
|
|
412
|
+
logger.debug(`正文图片已下载: ${localUrl}`);
|
|
367
413
|
}
|
|
368
414
|
catch (e) {
|
|
369
415
|
logger.warn(`正文图片下载失败: ${remoteUrl}`, e);
|
|
@@ -389,9 +435,7 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
389
435
|
message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
|
|
390
436
|
// 清理空行
|
|
391
437
|
const cleanMessage = message.split('\n').filter(line => line.trim() !== '' || line.includes('<')).join('\n');
|
|
392
|
-
|
|
393
|
-
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
394
|
-
}
|
|
438
|
+
logger.debug(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
395
439
|
const sendPromises = [];
|
|
396
440
|
// 发送主消息
|
|
397
441
|
if (cleanMessage) {
|
|
@@ -409,19 +453,17 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
409
453
|
const maxBytes = config.Max_size * 1024 * 1024;
|
|
410
454
|
if (sizeBytes !== null && sizeBytes > maxBytes) {
|
|
411
455
|
shouldSend = false;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
logger.info(`文件大小超限 (${sizeMB} MB > ${maxMB} MB),跳过: ${remoteUrl}`);
|
|
417
|
-
}
|
|
456
|
+
const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(2);
|
|
457
|
+
const maxMB = config.Max_size.toFixed(2);
|
|
458
|
+
sendPromises.push(session.send(`文件大小超限 (${sizeMB} MB > ${maxMB} MB)`));
|
|
459
|
+
logger.info(`文件大小超限 (${sizeMB} MB > ${maxMB} MB),跳过: ${remoteUrl}`);
|
|
418
460
|
}
|
|
419
461
|
}
|
|
420
462
|
if (shouldSend) {
|
|
421
463
|
try {
|
|
422
464
|
let localUrl = remoteUrl;
|
|
423
465
|
if (config.usingLocal)
|
|
424
|
-
localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
466
|
+
localUrl = await downloadAndMapUrl(ctx, remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger, config.enableCache);
|
|
425
467
|
if (!localUrl)
|
|
426
468
|
continue;
|
|
427
469
|
let element = null;
|
|
@@ -438,14 +480,10 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
438
480
|
}
|
|
439
481
|
if (element) {
|
|
440
482
|
sendPromises.push(session.send(element));
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const size = await getFileSize(remoteUrl, proxy, config.userAgent, logger);
|
|
446
|
-
const sizeMB = size ? (size / (1024 * 1024)).toFixed(2) : 'unknown';
|
|
447
|
-
logger.info(`${type} 已发送 (${sizeMB} MB): ${localUrl}`);
|
|
448
|
-
}
|
|
483
|
+
logger.debug(`${type} 直链 (${result.platform}): ${remoteUrl}`);
|
|
484
|
+
const size = await getFileSize(remoteUrl, proxy, config.userAgent, logger);
|
|
485
|
+
const sizeMB = size ? (size / (1024 * 1024)).toFixed(2) : 'unknown';
|
|
486
|
+
logger.debug(`${type} 已发送 (${sizeMB} MB): ${localUrl}`);
|
|
449
487
|
}
|
|
450
488
|
}
|
|
451
489
|
catch (e) {
|
|
@@ -456,10 +494,8 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
456
494
|
}
|
|
457
495
|
await Promise.all(sendPromises);
|
|
458
496
|
}
|
|
459
|
-
async function sendResult_forward(session, config, result, logger, mixed_sending = false) {
|
|
460
|
-
|
|
461
|
-
logger.info(mixed_sending ? '进入混合发送' : '进入合并发送');
|
|
462
|
-
}
|
|
497
|
+
async function sendResult_forward(ctx, session, config, result, logger, mixed_sending = false) {
|
|
498
|
+
logger.debug(mixed_sending ? '进入混合发送' : '进入合并发送');
|
|
463
499
|
const localDownloadDir = config.localDownloadDir;
|
|
464
500
|
const onebotReadDir = config.onebotReadDir;
|
|
465
501
|
let mediaCoverUrl = result.coverUrl;
|
|
@@ -473,7 +509,7 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
473
509
|
if (result.coverUrl) {
|
|
474
510
|
if (config.usingLocal) {
|
|
475
511
|
try {
|
|
476
|
-
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
512
|
+
mediaCoverUrl = await downloadAndMapUrl(ctx, result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger, config.enableCache);
|
|
477
513
|
}
|
|
478
514
|
catch (e) {
|
|
479
515
|
logger.warn('封面下载失败', e);
|
|
@@ -491,7 +527,7 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
491
527
|
await Promise.all(imgUrls.map(async (url) => {
|
|
492
528
|
if (config.usingLocal) {
|
|
493
529
|
try {
|
|
494
|
-
urlMap[url] = await downloadAndMapUrl(url, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
530
|
+
urlMap[url] = await downloadAndMapUrl(ctx, url, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger, config.enableCache);
|
|
495
531
|
}
|
|
496
532
|
catch (e) {
|
|
497
533
|
logger.warn(`正文图片下载失败: ${url}`, e);
|
|
@@ -507,7 +543,7 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
507
543
|
mediaMainbody = mediaMainbody.replace(new RegExp(escaped, 'g'), local);
|
|
508
544
|
}
|
|
509
545
|
}
|
|
510
|
-
// ===
|
|
546
|
+
// === 主消息 ===
|
|
511
547
|
let message = config.format;
|
|
512
548
|
message = message.replace(/{title}/g, result.title || '');
|
|
513
549
|
message = message.replace(/{authorName}/g, result.authorName || '');
|
|
@@ -555,7 +591,7 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
555
591
|
}
|
|
556
592
|
});
|
|
557
593
|
}
|
|
558
|
-
// --- 处理 files
|
|
594
|
+
// --- 处理 files ---
|
|
559
595
|
const extraSendPromises = [];
|
|
560
596
|
if (config.sendFiles && Array.isArray(result.files)) {
|
|
561
597
|
for (const file of result.files) {
|
|
@@ -568,30 +604,28 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
568
604
|
const maxBytes = config.Max_size * 1024 * 1024;
|
|
569
605
|
if (sizeBytes !== null && sizeBytes > maxBytes) {
|
|
570
606
|
shouldInclude = false;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
text: `文件大小超限 (${sizeMB} MB > ${maxMB} MB)`
|
|
582
|
-
}
|
|
607
|
+
const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(2);
|
|
608
|
+
const maxMB = config.Max_size.toFixed(2);
|
|
609
|
+
forwardNodes.push({
|
|
610
|
+
type: 'node',
|
|
611
|
+
data: {
|
|
612
|
+
user_id: session.selfId,
|
|
613
|
+
nickname: '分享助手',
|
|
614
|
+
content: {
|
|
615
|
+
type: 'text', data: {
|
|
616
|
+
text: `文件大小超限 (${sizeMB} MB > ${maxMB} MB)`
|
|
583
617
|
}
|
|
584
618
|
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
logger.info(`文件大小超限 (${sizeMB} MB > ${maxMB} MB),跳过: ${remoteUrl}`);
|
|
588
622
|
}
|
|
589
623
|
}
|
|
590
624
|
if (shouldInclude) {
|
|
591
625
|
try {
|
|
592
626
|
let localUrl = remoteUrl;
|
|
593
627
|
if (config.usingLocal)
|
|
594
|
-
localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
628
|
+
localUrl = await downloadAndMapUrl(ctx, remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger, config.enableCache);
|
|
595
629
|
if (!localUrl)
|
|
596
630
|
continue;
|
|
597
631
|
if (!mixed_sending) {
|
|
@@ -631,9 +665,7 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
631
665
|
extraSendPromises.push(session.send(element));
|
|
632
666
|
}
|
|
633
667
|
}
|
|
634
|
-
|
|
635
|
-
logger.info(`${type} 直链 (${result.platform}): ${remoteUrl}`);
|
|
636
|
-
}
|
|
668
|
+
logger.debug(`${type} 直链 (${result.platform}): ${remoteUrl}`);
|
|
637
669
|
}
|
|
638
670
|
catch (e) {
|
|
639
671
|
logger.warn(`${type} 下载失败: ${remoteUrl}`, e);
|
|
@@ -656,9 +688,7 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
656
688
|
}
|
|
657
689
|
if (forwardNodes.length === 0 && extraSendPromises.length === 0)
|
|
658
690
|
return;
|
|
659
|
-
|
|
660
|
-
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
661
|
-
}
|
|
691
|
+
logger.debug(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
662
692
|
if (!(session.onebot && session.onebot._request))
|
|
663
693
|
throw new Error("Onebot is not defined");
|
|
664
694
|
const promises = [];
|