koishi-plugin-share-links-analysis 0.5.0 → 0.5.2
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 -0
- package/lib/core.js +2 -0
- package/lib/index.js +8 -5
- package/lib/parsers/bilibili.js +6 -3
- package/lib/parsers/twitter.js +8 -1
- package/lib/parsers/xiaohongshu.js +5 -1
- package/lib/types.d.ts +7 -1
- package/lib/utils.d.ts +1 -1
- package/lib/utils.js +238 -160
- package/package.json +1 -1
- package/lib/parsers/tmp.json +0 -46
package/lib/core.d.ts
CHANGED
package/lib/core.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.parsers_str = void 0;
|
|
37
38
|
exports.resolveLinks = resolveLinks;
|
|
38
39
|
exports.processLink = processLink;
|
|
39
40
|
exports.init = init;
|
|
@@ -42,6 +43,7 @@ const Xiaohongshu = __importStar(require("./parsers/xiaohongshu"));
|
|
|
42
43
|
const Twitter = __importStar(require("./parsers/twitter"));
|
|
43
44
|
// 定义所有支持的解析器
|
|
44
45
|
const parsers = [Bilibili, Xiaohongshu, Twitter];
|
|
46
|
+
exports.parsers_str = ['bilibili', 'xiaohongshu', 'twitter'];
|
|
45
47
|
/**
|
|
46
48
|
* 从文本中解析出所有支持的链接
|
|
47
49
|
* @param content 消息内容
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
24
24
|
koishi_1.Schema.const('1').description('低清晰度优先'),
|
|
25
25
|
koishi_1.Schema.const('2').description('高清晰度优先'),
|
|
26
26
|
]).role('radio').default('1').description("发送的视频清晰度优先策略"),
|
|
27
|
-
Max_size: koishi_1.Schema.number().default(
|
|
27
|
+
Max_size: koishi_1.Schema.number().default(20).description("允许发送的最大文件大小(Mb)"),
|
|
28
28
|
Max_size_tip: koishi_1.Schema.string().default('文件体积过大,策略已阻止发送').description("对大文件的文字提示内容"),
|
|
29
29
|
MinimumTimeInterval: koishi_1.Schema.number().default(600).description("若干秒内不再处理相同链接,防止刷屏").min(1),
|
|
30
30
|
waitTip_Switch: koishi_1.Schema.union([
|
|
@@ -35,7 +35,8 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
35
35
|
koishi_1.Schema.const("plain").description("普通发送"),
|
|
36
36
|
koishi_1.Schema.const("forward").description("合并转发"),
|
|
37
37
|
koishi_1.Schema.const("mixed").description("混合发送"),
|
|
38
|
-
]).default("
|
|
38
|
+
]).default("forward").description("发送模式"),
|
|
39
|
+
sendFiles: koishi_1.Schema.boolean().default(true).description("是否发送文件(视频等)"),
|
|
39
40
|
}).description("基础设置"),
|
|
40
41
|
koishi_1.Schema.object({
|
|
41
42
|
format: koishi_1.Schema.string().role('textarea').default(`{title}
|
|
@@ -45,16 +46,18 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
45
46
|
----------
|
|
46
47
|
{mainbody}
|
|
47
48
|
----------
|
|
48
|
-
{sourceUrl}
|
|
49
|
-
{video}`).description('图文/视频输出格式。<br/>可用占位符: `{title}`, `{cover}`, `{authorName}`, `{mainbody}`, `{stats}`, `{sourceUrl}`, `{video}`, `{videoUrl}`'),
|
|
49
|
+
{sourceUrl}`).description('图文/视频输出格式。<br/>可用占位符: `{title}`, `{cover}`, `{authorName}`, `{mainbody}`, `{stats}`, `{sourceUrl}`'),
|
|
50
50
|
}).description("格式化模板"),
|
|
51
51
|
koishi_1.Schema.object({
|
|
52
52
|
parseLimit: koishi_1.Schema.number().default(3).description("单对话多链接解析上限"),
|
|
53
53
|
useNumeral: koishi_1.Schema.boolean().default(true).description("使用格式化数字 (如 10000 -> 1万)"),
|
|
54
54
|
showError: koishi_1.Schema.boolean().default(false).description("当链接不正确时提醒发送者"),
|
|
55
55
|
allow_sensitive: koishi_1.Schema.boolean().default(false).description("允许NSFW内容"),
|
|
56
|
-
proxy: koishi_1.Schema.string().description("代理设置"),
|
|
57
56
|
}).description("高级解析设置"),
|
|
57
|
+
koishi_1.Schema.object({
|
|
58
|
+
proxy: koishi_1.Schema.string().description("代理设置"),
|
|
59
|
+
proxy_settings: koishi_1.Schema.object(Object.fromEntries(core_1.parsers_str.map(parser => [parser, koishi_1.Schema.boolean().default(false)]))),
|
|
60
|
+
}).description("代理设置"),
|
|
58
61
|
koishi_1.Schema.object({
|
|
59
62
|
onebotReadDir: koishi_1.Schema.string().description('OneBot 实现 (如 NapCat) 所在的容器或环境提供的路径前缀。').default("/app/.config/QQ/NapCat/temp"),
|
|
60
63
|
localDownloadDir: koishi_1.Schema.string().description('与上述路径对应的、Koishi 所在的容器或主机可以访问的路径前缀。').default("/koishi/data/temp"),
|
package/lib/parsers/bilibili.js
CHANGED
|
@@ -174,15 +174,18 @@ async function process(ctx, config, link, session) {
|
|
|
174
174
|
const liked = (0, utils_1.numeral)(data.stat.like, config);
|
|
175
175
|
const coin = (0, utils_1.numeral)(data.stat.coin, config);
|
|
176
176
|
const favorite = (0, utils_1.numeral)(data.stat.favorite, config);
|
|
177
|
-
const statsString = `播放: ${play} | 弹幕: ${danmaku}
|
|
178
|
-
|
|
177
|
+
const statsString = `播放: ${play} | 弹幕: ${danmaku}\n点赞: ${liked} | 硬币: ${coin} | 收藏: ${favorite}`;
|
|
178
|
+
let files = [];
|
|
179
|
+
if (videoUrl) {
|
|
180
|
+
files = [{ type: "video", url: videoUrl }];
|
|
181
|
+
}
|
|
179
182
|
return {
|
|
180
183
|
platform: 'bilibili',
|
|
181
184
|
title: data.title,
|
|
182
185
|
authorName: data.owner.name,
|
|
183
186
|
mainbody: (0, utils_1.escapeHtml)(data.desc),
|
|
184
187
|
coverUrl: data.pic,
|
|
185
|
-
|
|
188
|
+
files: files,
|
|
186
189
|
sourceUrl: `https://www.bilibili.com/video/${data.bvid}`,
|
|
187
190
|
stats: statsString,
|
|
188
191
|
};
|
package/lib/parsers/twitter.js
CHANGED
|
@@ -104,6 +104,13 @@ async function process(ctx, config, link, session) {
|
|
|
104
104
|
}
|
|
105
105
|
const image = media?.images ? media?.images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
|
|
106
106
|
const mainbody = (0, utils_1.escapeHtml)(tweet_text) + image;
|
|
107
|
+
const videos = media?.videos;
|
|
108
|
+
let files = [];
|
|
109
|
+
if (videos) {
|
|
110
|
+
for (const video of videos) {
|
|
111
|
+
files.push({ type: "video", url: video.url });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
107
114
|
return {
|
|
108
115
|
platform: 'twitter',
|
|
109
116
|
title: `@${tweetData.user_screen_name} 的推文`,
|
|
@@ -111,7 +118,7 @@ async function process(ctx, config, link, session) {
|
|
|
111
118
|
mainbody: mainbody,
|
|
112
119
|
sourceUrl: link.url,
|
|
113
120
|
stats: statsString,
|
|
114
|
-
|
|
121
|
+
files: files,
|
|
115
122
|
coverUrl: media?.videos[0]?.preview_url,
|
|
116
123
|
};
|
|
117
124
|
}
|
|
@@ -263,13 +263,17 @@ async function process(ctx, config, link, session) {
|
|
|
263
263
|
const statsString = `点赞: ${liked} | 收藏: ${collected} | 评论: ${comment}`;
|
|
264
264
|
const image = images ? images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
|
|
265
265
|
const mainbody = (0, utils_1.escapeHtml)(noteData.desc.trim()) + image;
|
|
266
|
+
let files = [];
|
|
267
|
+
if (videoUrl) {
|
|
268
|
+
files = [{ type: "video", url: videoUrl }];
|
|
269
|
+
}
|
|
266
270
|
return {
|
|
267
271
|
platform: 'xiaohongshu',
|
|
268
272
|
title: noteData.title,
|
|
269
273
|
authorName: noteData.user.nickname,
|
|
270
274
|
mainbody: mainbody,
|
|
271
275
|
coverUrl: coverUrl,
|
|
272
|
-
|
|
276
|
+
files: files,
|
|
273
277
|
sourceUrl: urlToFetch,
|
|
274
278
|
stats: statsString,
|
|
275
279
|
};
|
package/lib/types.d.ts
CHANGED
|
@@ -10,10 +10,14 @@ export interface ParsedInfo {
|
|
|
10
10
|
authorName: string;
|
|
11
11
|
mainbody?: string;
|
|
12
12
|
coverUrl?: string;
|
|
13
|
-
|
|
13
|
+
files: FileInfo[];
|
|
14
14
|
sourceUrl: string;
|
|
15
15
|
stats: string;
|
|
16
16
|
}
|
|
17
|
+
export interface FileInfo {
|
|
18
|
+
type: 'video' | 'audio' | 'generic';
|
|
19
|
+
url: string;
|
|
20
|
+
}
|
|
17
21
|
export interface PluginConfig {
|
|
18
22
|
Video_ClarityPriority: '1' | '2';
|
|
19
23
|
Max_size: number;
|
|
@@ -21,12 +25,14 @@ export interface PluginConfig {
|
|
|
21
25
|
Min_Interval: number;
|
|
22
26
|
waitTip_Switch: false | string;
|
|
23
27
|
useForward: 'plain' | 'forward' | 'mixed';
|
|
28
|
+
sendFiles: boolean;
|
|
24
29
|
format: string;
|
|
25
30
|
parseLimit: number;
|
|
26
31
|
useNumeral: boolean;
|
|
27
32
|
showError: boolean;
|
|
28
33
|
allow_sensitive: boolean;
|
|
29
34
|
proxy: string;
|
|
35
|
+
proxy_settings: object;
|
|
30
36
|
onebotReadDir: string;
|
|
31
37
|
localDownloadDir: string;
|
|
32
38
|
userAgent: string;
|
package/lib/utils.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ import { Logger, Session } from "koishi";
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function numeral(num: number, config: PluginConfig): string;
|
|
10
10
|
export declare function escapeHtml(str: string): string;
|
|
11
|
-
export declare function getFileSize(url: string, proxy: string | undefined, userAgent: string | undefined): Promise<number | null>;
|
|
11
|
+
export declare function getFileSize(url: string, proxy: string | undefined, userAgent: string | undefined, logger: Logger): Promise<number | null>;
|
|
12
12
|
export declare function sendResult_plain(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger): Promise<void>;
|
|
13
13
|
export declare function sendResult_forward(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger, mixed_sending?: boolean): Promise<void>;
|
package/lib/utils.js
CHANGED
|
@@ -170,7 +170,22 @@ async function downloadAndMapUrl(url, proxy, userAgent, localDownloadDir, onebot
|
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
|
-
async function getFileSize(url, proxy, userAgent) {
|
|
173
|
+
async function getFileSize(url, proxy, userAgent, logger) {
|
|
174
|
+
try {
|
|
175
|
+
// 先尝试HEAD请求(标准方式)
|
|
176
|
+
const headSize = await tryHeadRequest(url, proxy, userAgent, logger);
|
|
177
|
+
if (headSize !== null) {
|
|
178
|
+
return headSize;
|
|
179
|
+
}
|
|
180
|
+
// HEAD失败,尝试GET请求获取部分内容
|
|
181
|
+
return await tryGetRequestForSize(url, proxy, userAgent, logger);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
logger.warn(`获取文件大小失败: ${url}`, e);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function tryHeadRequest(url, proxy, userAgent, logger) {
|
|
174
189
|
return new Promise((resolve) => {
|
|
175
190
|
const u = new url_1.URL(url);
|
|
176
191
|
const agent = getProxyAgent(proxy, url);
|
|
@@ -180,7 +195,8 @@ async function getFileSize(url, proxy, userAgent) {
|
|
|
180
195
|
method: 'HEAD',
|
|
181
196
|
timeout: 10000,
|
|
182
197
|
headers: {
|
|
183
|
-
'User-Agent': userAgent
|
|
198
|
+
'User-Agent': userAgent,
|
|
199
|
+
'Referer': 'https://www.bilibili.com/'
|
|
184
200
|
}
|
|
185
201
|
}, (res) => {
|
|
186
202
|
const len = res.headers['content-length'];
|
|
@@ -192,11 +208,61 @@ async function getFileSize(url, proxy, userAgent) {
|
|
|
192
208
|
}
|
|
193
209
|
req.destroy();
|
|
194
210
|
});
|
|
195
|
-
req.on('error', () => {
|
|
211
|
+
req.on('error', (err) => {
|
|
212
|
+
logger.warn(`HEAD请求失败: ${url}`, err);
|
|
213
|
+
req.destroy();
|
|
214
|
+
resolve(null);
|
|
215
|
+
});
|
|
216
|
+
req.on('timeout', () => {
|
|
217
|
+
logger.warn(`HEAD请求超时: ${url}`);
|
|
218
|
+
req.destroy();
|
|
219
|
+
resolve(null);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async function tryGetRequestForSize(url, proxy, userAgent, logger) {
|
|
224
|
+
proxy = undefined;
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
const u = new url_1.URL(url);
|
|
227
|
+
const agent = getProxyAgent(proxy, url);
|
|
228
|
+
const getter = u.protocol === 'https:' ? require('https').get : require('http').get;
|
|
229
|
+
const req = getter(url, {
|
|
230
|
+
agent,
|
|
231
|
+
timeout: 15000,
|
|
232
|
+
headers: {
|
|
233
|
+
'User-Agent': userAgent,
|
|
234
|
+
'Range': 'bytes=0-1023' // 只请求前1KB
|
|
235
|
+
}
|
|
236
|
+
}, (res) => {
|
|
237
|
+
const contentRange = res.headers['content-range'];
|
|
238
|
+
if (contentRange) {
|
|
239
|
+
// 从 Content-Range 头获取总大小,例如: "bytes 0-1023/12345678"
|
|
240
|
+
const match = contentRange.match(/\/(\d+)$/);
|
|
241
|
+
if (match) {
|
|
242
|
+
resolve(parseInt(match[1], 10));
|
|
243
|
+
req.destroy();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const len = res.headers['content-length'];
|
|
248
|
+
if (len && /^\d+$/.test(len)) {
|
|
249
|
+
resolve(parseInt(len, 10));
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
resolve(null);
|
|
253
|
+
}
|
|
254
|
+
// 读取少量数据后关闭连接
|
|
255
|
+
res.on('data', () => {
|
|
256
|
+
req.destroy();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
req.on('error', (err) => {
|
|
260
|
+
logger.warn(`GET请求获取大小失败: ${url}`, err);
|
|
196
261
|
req.destroy();
|
|
197
262
|
resolve(null);
|
|
198
263
|
});
|
|
199
264
|
req.on('timeout', () => {
|
|
265
|
+
logger.warn(`GET请求获取大小超时: ${url}`);
|
|
200
266
|
req.destroy();
|
|
201
267
|
resolve(null);
|
|
202
268
|
});
|
|
@@ -209,12 +275,16 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
209
275
|
const localDownloadDir = config.localDownloadDir;
|
|
210
276
|
const onebotReadDir = config.onebotReadDir;
|
|
211
277
|
let mediaCoverUrl = result.coverUrl;
|
|
212
|
-
let mediaVideoUrl = result.videoUrl || null;
|
|
213
278
|
let mediaMainbody = result.mainbody;
|
|
279
|
+
let proxy = undefined;
|
|
280
|
+
if (config.proxy_settings[result.platform]) {
|
|
281
|
+
proxy = config.proxy;
|
|
282
|
+
logger.info("正在使用代理");
|
|
283
|
+
}
|
|
214
284
|
// --- 下载封面 ---
|
|
215
285
|
if (result.coverUrl) {
|
|
216
286
|
try {
|
|
217
|
-
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl,
|
|
287
|
+
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
218
288
|
if (config.logLevel === 'full')
|
|
219
289
|
logger.info(`封面已下载: ${mediaCoverUrl}`);
|
|
220
290
|
}
|
|
@@ -223,41 +293,6 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
223
293
|
mediaCoverUrl = '';
|
|
224
294
|
}
|
|
225
295
|
}
|
|
226
|
-
// --- 视频:先检查大小 ---
|
|
227
|
-
let videoExceedsLimit = false;
|
|
228
|
-
if (result.videoUrl) {
|
|
229
|
-
const sizeBytes = await getFileSize(result.videoUrl, config.proxy, config.userAgent);
|
|
230
|
-
const maxBytes = config.Max_size !== undefined ? config.Max_size * 1024 * 1024 : undefined;
|
|
231
|
-
// 日志用 MB(保留 2 位小数)
|
|
232
|
-
const formatMB = (bytes) => (bytes / (1024 * 1024)).toFixed(2);
|
|
233
|
-
if (sizeBytes === null) {
|
|
234
|
-
logger.warn(`无法获取视频大小: ${result.videoUrl},默认允许下载`);
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
const sizeMB = formatMB(sizeBytes);
|
|
238
|
-
if (maxBytes !== undefined && sizeBytes > maxBytes) {
|
|
239
|
-
videoExceedsLimit = true;
|
|
240
|
-
mediaVideoUrl = null;
|
|
241
|
-
const maxMB = config.Max_size.toFixed(2);
|
|
242
|
-
if (config.logLevel !== 'none') {
|
|
243
|
-
logger.info(`视频大小超限 (${sizeMB} MB > ${maxMB} MB): ${result.videoUrl}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
// 大小合规,执行下载
|
|
248
|
-
try {
|
|
249
|
-
mediaVideoUrl = await downloadAndMapUrl(result.videoUrl, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
250
|
-
if (config.logLevel === 'full') {
|
|
251
|
-
logger.info(`视频已下载 (${sizeMB} MB): ${mediaVideoUrl}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
catch (e) {
|
|
255
|
-
logger.warn(`视频下载失败: ${result.videoUrl}`, e);
|
|
256
|
-
mediaVideoUrl = null;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
296
|
// --- 下载 mainbody 中的图片 ---
|
|
262
297
|
if (result.mainbody) {
|
|
263
298
|
const imgMatches = [...result.mainbody.matchAll(/<img\s[^>]*src\s*=\s*["']?([^"'>\s]+)["']?/gi)];
|
|
@@ -265,7 +300,7 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
265
300
|
await Promise.all(imgMatches.map(async (match) => {
|
|
266
301
|
const remoteUrl = match[1];
|
|
267
302
|
try {
|
|
268
|
-
const localUrl = await downloadAndMapUrl(remoteUrl,
|
|
303
|
+
const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
269
304
|
urlMap[remoteUrl] = localUrl;
|
|
270
305
|
if (config.logLevel === 'full')
|
|
271
306
|
logger.info(`正文图片已下载: ${localUrl}`);
|
|
@@ -288,34 +323,72 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
288
323
|
message = message.replace(/{sourceUrl}/g, escapeHtml(result.sourceUrl || ''));
|
|
289
324
|
message = message.replace(/{cover}/g, mediaCoverUrl ? koishi_1.h.image(mediaCoverUrl).toString() : '');
|
|
290
325
|
message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
|
|
291
|
-
//
|
|
292
|
-
if (result.videoUrl) {
|
|
293
|
-
message = message.replace(/{videoUrl}/g, escapeHtml(result.videoUrl));
|
|
294
|
-
if (videoExceedsLimit) {
|
|
295
|
-
const tip = escapeHtml(config.Max_size_tip);
|
|
296
|
-
message = message.replace(/{video}/g, tip);
|
|
297
|
-
}
|
|
298
|
-
else if (mediaVideoUrl) {
|
|
299
|
-
message = message.replace(/{video}/g, koishi_1.h.video(mediaVideoUrl).toString());
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
message = message.replace(/{video}/g, '');
|
|
303
|
-
}
|
|
304
|
-
if (config.logLevel === 'link_only') {
|
|
305
|
-
logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
message = message.replace(/{video}/g, '');
|
|
310
|
-
message = message.replace(/{videoUrl}/g, '');
|
|
311
|
-
}
|
|
326
|
+
// 清理空行
|
|
312
327
|
const cleanMessage = message.split('\n').filter(line => line.trim() !== '' || line.includes('<')).join('\n');
|
|
313
328
|
if (config.logLevel === 'full') {
|
|
314
329
|
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
315
330
|
}
|
|
331
|
+
const sendPromises = [];
|
|
332
|
+
// 发送主消息
|
|
316
333
|
if (cleanMessage) {
|
|
317
|
-
|
|
334
|
+
sendPromises.push(session.send(koishi_1.h.quote(session.messageId) + cleanMessage));
|
|
335
|
+
}
|
|
336
|
+
// --- 发送 files 中的所有媒体(video/audio/generic)---
|
|
337
|
+
if (config.sendFiles && Array.isArray(result.files)) {
|
|
338
|
+
for (const file of result.files) {
|
|
339
|
+
const { type, url: remoteUrl } = file;
|
|
340
|
+
if (!['video', 'audio', 'generic'].includes(type))
|
|
341
|
+
continue;
|
|
342
|
+
let shouldSend = true;
|
|
343
|
+
if (config.Max_size !== undefined) {
|
|
344
|
+
const sizeBytes = await getFileSize(remoteUrl, proxy, config.userAgent, logger);
|
|
345
|
+
const maxBytes = config.Max_size * 1024 * 1024;
|
|
346
|
+
if (sizeBytes !== null && sizeBytes > maxBytes) {
|
|
347
|
+
shouldSend = false;
|
|
348
|
+
if (config.logLevel !== 'none') {
|
|
349
|
+
const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(2);
|
|
350
|
+
const maxMB = config.Max_size.toFixed(2);
|
|
351
|
+
sendPromises.push(session.send(`文件大小超限 (${sizeMB} MB > ${maxMB} MB)`));
|
|
352
|
+
logger.info(`文件大小超限 (${sizeMB} MB > ${maxMB} MB),跳过: ${remoteUrl}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (shouldSend) {
|
|
357
|
+
try {
|
|
358
|
+
const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
359
|
+
if (!localUrl)
|
|
360
|
+
continue;
|
|
361
|
+
let element = null;
|
|
362
|
+
if (type === 'video') {
|
|
363
|
+
element = koishi_1.h.video(localUrl).toString();
|
|
364
|
+
}
|
|
365
|
+
else if (type === 'audio') {
|
|
366
|
+
element = koishi_1.h.audio(localUrl).toString();
|
|
367
|
+
}
|
|
368
|
+
else if (type === 'generic') {
|
|
369
|
+
// 注意:标准 OneBot v11 不支持 file,部分实现支持
|
|
370
|
+
// 若你环境不支持,可改用文本链接:element = escapeHtml(remoteUrl);
|
|
371
|
+
element = koishi_1.h.file(localUrl).toString();
|
|
372
|
+
}
|
|
373
|
+
if (element) {
|
|
374
|
+
sendPromises.push(session.send(element));
|
|
375
|
+
if (config.logLevel === 'link_only') {
|
|
376
|
+
logger.info(`${type} 直链 (${result.platform}): ${remoteUrl}`);
|
|
377
|
+
}
|
|
378
|
+
if (config.logLevel === 'full') {
|
|
379
|
+
const size = await getFileSize(remoteUrl, proxy, config.userAgent, logger);
|
|
380
|
+
const sizeMB = size ? (size / (1024 * 1024)).toFixed(2) : 'unknown';
|
|
381
|
+
logger.info(`${type} 已发送 (${sizeMB} MB): ${localUrl}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
logger.warn(`${type} 下载/发送失败: ${remoteUrl}`, e);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
318
390
|
}
|
|
391
|
+
await Promise.all(sendPromises);
|
|
319
392
|
}
|
|
320
393
|
async function sendResult_forward(session, config, result, logger, mixed_sending = false) {
|
|
321
394
|
if (config.logLevel === 'full') {
|
|
@@ -324,58 +397,29 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
324
397
|
const localDownloadDir = config.localDownloadDir;
|
|
325
398
|
const onebotReadDir = config.onebotReadDir;
|
|
326
399
|
let mediaCoverUrl = result.coverUrl;
|
|
327
|
-
let mediaVideoUrl = result.videoUrl || null;
|
|
328
400
|
let mediaMainbody = result.mainbody;
|
|
401
|
+
let proxy = undefined;
|
|
402
|
+
if (config.proxy_settings[result.platform]) {
|
|
403
|
+
proxy = config.proxy;
|
|
404
|
+
logger.info("正在使用代理");
|
|
405
|
+
}
|
|
329
406
|
// --- 封面 ---
|
|
330
407
|
if (result.coverUrl) {
|
|
331
408
|
try {
|
|
332
|
-
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl,
|
|
409
|
+
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
333
410
|
}
|
|
334
411
|
catch (e) {
|
|
335
412
|
logger.warn('封面下载失败', e);
|
|
336
413
|
mediaCoverUrl = '';
|
|
337
414
|
}
|
|
338
415
|
}
|
|
339
|
-
// --- 视频大小检查 + 下载 ---
|
|
340
|
-
let videoExceedsLimit = false;
|
|
341
|
-
if (result.videoUrl) {
|
|
342
|
-
const sizeBytes = await getFileSize(result.videoUrl, config.proxy, config.userAgent);
|
|
343
|
-
const maxBytes = config.Max_size !== undefined ? config.Max_size * 1024 * 1024 : undefined;
|
|
344
|
-
const formatMB = (bytes) => (bytes / (1024 * 1024)).toFixed(2);
|
|
345
|
-
if (sizeBytes === null) {
|
|
346
|
-
logger.warn(`无法获取视频大小: ${result.videoUrl},默认允许下载`);
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
const sizeMB = formatMB(sizeBytes);
|
|
350
|
-
if (maxBytes !== undefined && sizeBytes > maxBytes) {
|
|
351
|
-
videoExceedsLimit = true;
|
|
352
|
-
mediaVideoUrl = null;
|
|
353
|
-
const maxMB = config.Max_size.toFixed(2);
|
|
354
|
-
if (config.logLevel !== 'none') {
|
|
355
|
-
logger.info(`视频大小超限 (${sizeMB} MB > ${maxMB} MB): ${result.videoUrl}`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
try {
|
|
360
|
-
mediaVideoUrl = await downloadAndMapUrl(result.videoUrl, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
361
|
-
if (config.logLevel === 'full') {
|
|
362
|
-
logger.info(`视频已下载 (${sizeMB} MB): ${mediaVideoUrl}`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
catch (e) {
|
|
366
|
-
logger.warn('视频下载失败', e);
|
|
367
|
-
mediaVideoUrl = null;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
416
|
// --- mainbody 图片 ---
|
|
373
417
|
if (result.mainbody) {
|
|
374
418
|
const imgUrls = [...result.mainbody.matchAll(/<img\s[^>]*src\s*=\s*["']?([^"'>\s]+)["']?/gi)].map(m => m[1]);
|
|
375
419
|
const urlMap = {};
|
|
376
420
|
await Promise.all(imgUrls.map(async (url) => {
|
|
377
421
|
try {
|
|
378
|
-
urlMap[url] = await downloadAndMapUrl(url,
|
|
422
|
+
urlMap[url] = await downloadAndMapUrl(url, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
379
423
|
}
|
|
380
424
|
catch (e) {
|
|
381
425
|
logger.warn(`正文图片下载失败: ${url}`, e);
|
|
@@ -387,98 +431,130 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
387
431
|
mediaMainbody = mediaMainbody.replace(new RegExp(escaped, 'g'), local);
|
|
388
432
|
}
|
|
389
433
|
}
|
|
390
|
-
// ===
|
|
434
|
+
// === 主消息(不含媒体文件)===
|
|
391
435
|
let message = config.format;
|
|
392
436
|
message = message.replace(/{title}/g, escapeHtml(result.title || ''));
|
|
393
437
|
message = message.replace(/{authorName}/g, escapeHtml(result.authorName || ''));
|
|
438
|
+
message = message.replace(/{mainbody}/g, mediaMainbody ?? '');
|
|
394
439
|
message = message.replace(/{sourceUrl}/g, escapeHtml(result.sourceUrl || ''));
|
|
440
|
+
message = message.replace(/{cover}/g, mediaCoverUrl ? koishi_1.h.image(mediaCoverUrl).toString() : '');
|
|
395
441
|
message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
|
|
396
|
-
// 处理 {videoUrl} 和 {video} 占位符逻辑(用于后续判断)
|
|
397
|
-
if (result.videoUrl) {
|
|
398
|
-
message = message.replace(/{videoUrl}/g, escapeHtml(result.videoUrl));
|
|
399
|
-
if (videoExceedsLimit) {
|
|
400
|
-
const tip = escapeHtml(config.Max_size_tip);
|
|
401
|
-
message = message.replace(/{video}/g, tip);
|
|
402
|
-
}
|
|
403
|
-
// 注意:这里不替换 {video} 为实际视频,留到转发节点构建时处理
|
|
404
|
-
}
|
|
405
|
-
const hasVideoInTemplate = message.includes('{video}');
|
|
406
|
-
const mediaMap = {};
|
|
407
|
-
if (mediaCoverUrl) {
|
|
408
|
-
mediaMap['{cover}'] = [{ type: 'image', data: { file: mediaCoverUrl } }];
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
mediaMap['{cover}'] = [];
|
|
412
|
-
}
|
|
413
442
|
const lines = message.split('\n').filter(line => line.trim() !== '');
|
|
414
|
-
const
|
|
443
|
+
const mainSegments = [];
|
|
415
444
|
for (let i = 0; i < lines.length; i++) {
|
|
416
445
|
const line = lines[i];
|
|
417
446
|
const isLastLine = i === lines.length - 1;
|
|
418
|
-
const tokens = line.split(/(\{cover\}
|
|
447
|
+
const tokens = line.split(/(\{cover\})/g);
|
|
419
448
|
const currentLineSegments = [];
|
|
420
449
|
let hasTextContent = false;
|
|
421
450
|
for (const token of tokens) {
|
|
422
451
|
if (token === '{cover}') {
|
|
423
|
-
|
|
452
|
+
if (mediaCoverUrl) {
|
|
453
|
+
currentLineSegments.push({ type: 'image', data: { file: mediaCoverUrl } });
|
|
454
|
+
}
|
|
424
455
|
}
|
|
425
456
|
else if (token === '{mainbody}') {
|
|
426
457
|
const parsed = parseHtmlToSegments(mediaMainbody || '');
|
|
427
458
|
currentLineSegments.push(...parsed);
|
|
428
459
|
hasTextContent = parsed.some(seg => seg.type === 'text');
|
|
429
460
|
}
|
|
430
|
-
else if (token === '{video}') {
|
|
431
|
-
// 超限时替换为提示文本;否则留空(由转发节点处理)
|
|
432
|
-
if (videoExceedsLimit) {
|
|
433
|
-
const tip = config.Max_size_tip;
|
|
434
|
-
currentLineSegments.push({ type: 'text', data: { text: tip } });
|
|
435
|
-
hasTextContent = true;
|
|
436
|
-
}
|
|
437
|
-
// 否则不插入内容(视频将作为独立节点)
|
|
438
|
-
}
|
|
439
461
|
else if (token.trim() !== '') {
|
|
440
462
|
currentLineSegments.push({ type: 'text', data: { text: token } });
|
|
441
463
|
hasTextContent = true;
|
|
442
464
|
}
|
|
443
465
|
}
|
|
444
466
|
if (currentLineSegments.length > 0) {
|
|
445
|
-
|
|
467
|
+
mainSegments.push(...currentLineSegments);
|
|
446
468
|
}
|
|
447
469
|
if (!isLastLine && hasTextContent) {
|
|
448
|
-
|
|
470
|
+
mainSegments.push({ type: 'text', data: { text: '\n' } });
|
|
449
471
|
}
|
|
450
472
|
}
|
|
451
473
|
const forwardNodes = [];
|
|
452
|
-
if (
|
|
474
|
+
if (mainSegments.length > 0) {
|
|
453
475
|
forwardNodes.push({
|
|
454
476
|
type: 'node',
|
|
455
477
|
data: {
|
|
456
478
|
user_id: session.selfId,
|
|
457
479
|
nickname: '分享助手',
|
|
458
|
-
content:
|
|
480
|
+
content: mainSegments
|
|
459
481
|
}
|
|
460
482
|
});
|
|
461
483
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
484
|
+
// --- 处理 files 中的所有媒体 ---
|
|
485
|
+
const extraSendPromises = [];
|
|
486
|
+
if (config.sendFiles && Array.isArray(result.files)) {
|
|
487
|
+
for (const file of result.files) {
|
|
488
|
+
const { type, url: remoteUrl } = file;
|
|
489
|
+
if (!['video', 'audio', 'generic'].includes(type))
|
|
490
|
+
continue;
|
|
491
|
+
let shouldInclude = true;
|
|
492
|
+
if (config.Max_size !== undefined) {
|
|
493
|
+
const sizeBytes = await getFileSize(remoteUrl, proxy, config.userAgent, logger);
|
|
494
|
+
const maxBytes = config.Max_size * 1024 * 1024;
|
|
495
|
+
if (sizeBytes !== null && sizeBytes > maxBytes) {
|
|
496
|
+
shouldInclude = false;
|
|
497
|
+
if (config.logLevel !== 'none') {
|
|
498
|
+
const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(2);
|
|
499
|
+
const maxMB = config.Max_size.toFixed(2);
|
|
500
|
+
extraSendPromises.push(session.send(`文件大小超限 (${sizeMB} MB > ${maxMB} MB)`));
|
|
501
|
+
logger.info(`文件大小超限 (${sizeMB} MB > ${maxMB} MB),跳过: ${remoteUrl}`);
|
|
502
|
+
}
|
|
471
503
|
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
504
|
+
}
|
|
505
|
+
if (shouldInclude) {
|
|
506
|
+
try {
|
|
507
|
+
const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
508
|
+
if (!localUrl)
|
|
509
|
+
continue;
|
|
510
|
+
if (!mixed_sending) {
|
|
511
|
+
// 作为转发节点发送
|
|
512
|
+
let segment = null;
|
|
513
|
+
if (type === 'video') {
|
|
514
|
+
segment = { type: 'video', data: { file: localUrl } };
|
|
515
|
+
}
|
|
516
|
+
else if (type === 'audio') {
|
|
517
|
+
segment = { type: 'audio', data: { file: localUrl } };
|
|
518
|
+
}
|
|
519
|
+
else if (type === 'generic') {
|
|
520
|
+
// 注意:标准 OneBot 转发节点不支持 file,这里降级为文本链接
|
|
521
|
+
segment = { type: 'text', data: { text: `📄 文件: ${remoteUrl}` } };
|
|
522
|
+
}
|
|
523
|
+
if (segment) {
|
|
524
|
+
forwardNodes.push({
|
|
525
|
+
type: 'node',
|
|
526
|
+
data: {
|
|
527
|
+
user_id: session.selfId,
|
|
528
|
+
nickname: '分享助手',
|
|
529
|
+
content: [segment]
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
// 混合模式:独立发送
|
|
536
|
+
let element = null;
|
|
537
|
+
if (type === 'video')
|
|
538
|
+
element = koishi_1.h.video(localUrl).toString();
|
|
539
|
+
else if (type === 'audio')
|
|
540
|
+
element = koishi_1.h.audio(localUrl).toString();
|
|
541
|
+
else if (type === 'generic')
|
|
542
|
+
element = koishi_1.h.file(localUrl).toString();
|
|
543
|
+
if (element) {
|
|
544
|
+
extraSendPromises.push(session.send(element));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (config.logLevel === 'link_only') {
|
|
548
|
+
logger.info(`${type} 直链 (${result.platform}): ${remoteUrl}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (e) {
|
|
552
|
+
logger.warn(`${type} 下载失败: ${remoteUrl}`, e);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
479
555
|
}
|
|
480
556
|
}
|
|
481
|
-
if (forwardNodes.length === 0)
|
|
557
|
+
if (forwardNodes.length === 0 && extraSendPromises.length === 0)
|
|
482
558
|
return;
|
|
483
559
|
if (config.logLevel === 'full') {
|
|
484
560
|
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
@@ -486,16 +562,18 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
486
562
|
if (!(session.onebot && session.onebot._request))
|
|
487
563
|
throw new Error("Onebot is not defined");
|
|
488
564
|
const promises = [];
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
565
|
+
if (forwardNodes.length > 0) {
|
|
566
|
+
promises.push(session.onebot._request('send_group_forward_msg', {
|
|
567
|
+
group_id: session.guildId,
|
|
568
|
+
messages: forwardNodes,
|
|
569
|
+
news: [{ text: mediaMainbody || '-' }, { text: '点击查看详情 | Powered by furryaxw' }],
|
|
570
|
+
prompt: result.title || '',
|
|
571
|
+
summary: '分享解析',
|
|
572
|
+
source: result.title || ''
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
if (mixed_sending && extraSendPromises.length > 0) {
|
|
576
|
+
promises.push(...extraSendPromises);
|
|
499
577
|
}
|
|
500
578
|
await Promise.all(promises);
|
|
501
579
|
}
|
package/package.json
CHANGED
package/lib/parsers/tmp.json
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
allSameType: true,
|
|
3
|
-
article: null,
|
|
4
|
-
combinedMediaUrl: null,
|
|
5
|
-
communityNote: null,
|
|
6
|
-
conversationID: '1988209837298377142',
|
|
7
|
-
date: 'Tue Nov 11 11:38:59 +0000 2025',
|
|
8
|
-
date_epoch: 1762861139,
|
|
9
|
-
fetched_on: 1763038701,
|
|
10
|
-
hasMedia: true,
|
|
11
|
-
hashtags: [],
|
|
12
|
-
lang: 'zxx',
|
|
13
|
-
likes: 80,
|
|
14
|
-
mediaURLs: [
|
|
15
|
-
'https://pbs.twimg.com/media/G5eKTetbcAE9wtk.jpg'
|
|
16
|
-
],
|
|
17
|
-
media_extended: [
|
|
18
|
-
{
|
|
19
|
-
altText: null,
|
|
20
|
-
id_str: '1988209827773116417',
|
|
21
|
-
size: {
|
|
22
|
-
height: 1840,
|
|
23
|
-
width: 1840
|
|
24
|
-
},
|
|
25
|
-
thumbnail_url: 'https://pbs.twimg.com/media/G5eKTetbcAE9wtk.jpg',
|
|
26
|
-
type: 'image',
|
|
27
|
-
url: 'https://pbs.twimg.com/media/G5eKTetbcAE9wtk.jpg'
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
pollData: null,
|
|
31
|
-
possibly_sensitive: false,
|
|
32
|
-
qrt: null,
|
|
33
|
-
qrtURL: null,
|
|
34
|
-
replies: 5,
|
|
35
|
-
replyingTo: null,
|
|
36
|
-
replyingToID: null,
|
|
37
|
-
retweet: null,
|
|
38
|
-
retweetURL: null,
|
|
39
|
-
retweets: 4,
|
|
40
|
-
text: 'https://t.co/b8AyvpjhAE',
|
|
41
|
-
tweetID: '1988209837298377142',
|
|
42
|
-
tweetURL: 'https://twitter.com/moyushuang_/status/1988209837298377142',
|
|
43
|
-
user_name: '魔芋凉粉',
|
|
44
|
-
user_profile_image_url: 'https://pbs.twimg.com/profile_images/1830265587676962816/KM46JNnI_normal.jpg',
|
|
45
|
-
user_screen_name: 'moyushuang_'
|
|
46
|
-
}
|