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 CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Context, Session } from 'koishi';
2
2
  import { Link, PluginConfig, ParsedInfo } from './types';
3
+ export declare const parsers_str: string[];
3
4
  /**
4
5
  * 从文本中解析出所有支持的链接
5
6
  * @param content 消息内容
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(5).description("允许发送的最大文件大小(Mb)"),
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("mixed").description("发送模式"),
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"),
@@ -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
- 点赞: ${liked} | 硬币: ${coin} | 收藏: ${favorite}`;
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
- videoUrl: videoUrl,
188
+ files: files,
186
189
  sourceUrl: `https://www.bilibili.com/video/${data.bvid}`,
187
190
  stats: statsString,
188
191
  };
@@ -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
- videoUrl: media?.videos[0]?.url, // 取第一个视频
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
- videoUrl: videoUrl,
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
- videoUrl?: string | null;
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, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
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, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
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
- await session.send(koishi_1.h.quote(session.messageId) + cleanMessage);
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, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
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, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
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 nonVideoSegments = [];
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\}|\{video\})/g);
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
- currentLineSegments.push(...mediaMap[token]);
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
- nonVideoSegments.push(...currentLineSegments);
467
+ mainSegments.push(...currentLineSegments);
446
468
  }
447
469
  if (!isLastLine && hasTextContent) {
448
- nonVideoSegments.push({ type: 'text', data: { text: '\n' } });
470
+ mainSegments.push({ type: 'text', data: { text: '\n' } });
449
471
  }
450
472
  }
451
473
  const forwardNodes = [];
452
- if (nonVideoSegments.length > 0) {
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: nonVideoSegments
480
+ content: mainSegments
459
481
  }
460
482
  });
461
483
  }
462
- let videoElement;
463
- if (hasVideoInTemplate && result.videoUrl && !videoExceedsLimit && mediaVideoUrl) {
464
- if (!mixed_sending) {
465
- forwardNodes.push({
466
- type: 'node',
467
- data: {
468
- user_id: session.selfId,
469
- nickname: '分享助手',
470
- content: [{ type: 'video', data: { file: mediaVideoUrl } }]
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
- else {
475
- videoElement = koishi_1.h.video(mediaVideoUrl).toString();
476
- }
477
- if (config.logLevel === 'link_only') {
478
- logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
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
- promises.push(session.onebot._request('send_group_forward_msg', {
490
- group_id: session.guildId,
491
- messages: forwardNodes,
492
- news: [{ text: mediaMainbody || '-' }, { text: '点击查看详情 | Powered by furryaxw' }],
493
- prompt: result.title || '',
494
- summary: '分享解析',
495
- source: result.title || ''
496
- }));
497
- if (mixed_sending && videoElement) {
498
- promises.push(session.send(videoElement));
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
@@ -2,7 +2,7 @@
2
2
  "name": "koishi-plugin-share-links-analysis",
3
3
  "description": "自用插件",
4
4
  "license": "MIT",
5
- "version": "0.5.0",
5
+ "version": "0.5.2",
6
6
  "main": "lib/index.js",
7
7
  "typings": "lib/index.d.ts",
8
8
  "files": [
@@ -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
- }