koishi-plugin-share-links-analysis 0.4.0 → 0.5.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.js CHANGED
@@ -39,8 +39,9 @@ exports.processLink = processLink;
39
39
  exports.init = init;
40
40
  const Bilibili = __importStar(require("./parsers/bilibili"));
41
41
  const Xiaohongshu = __importStar(require("./parsers/xiaohongshu"));
42
+ const Twitter = __importStar(require("./parsers/twitter"));
42
43
  // 定义所有支持的解析器
43
- const parsers = [Bilibili, Xiaohongshu];
44
+ const parsers = [Bilibili, Xiaohongshu, Twitter];
44
45
  /**
45
46
  * 从文本中解析出所有支持的链接
46
47
  * @param content 消息内容
package/lib/index.js CHANGED
@@ -24,8 +24,8 @@ 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
- Maximumduration: koishi_1.Schema.number().default(5).description("允许解析的视频最大时长(分钟)").min(1),
28
- Maximumduration_tip: koishi_1.Schema.string().default('视频太长啦!还是去平台官网看吧~').description("对过长视频的文字提示内容"),
27
+ Max_size: koishi_1.Schema.number().default(5).description("允许发送的最大文件大小(Mb)"),
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([
31
31
  koishi_1.Schema.const(false).description('不返回文字提示'),
@@ -43,17 +43,22 @@ exports.Config = koishi_1.Schema.intersect([
43
43
  作者:{authorName}
44
44
  {stats}
45
45
  ----------
46
- {description}
47
- {images}
46
+ {mainbody}
48
47
  ----------
49
48
  {sourceUrl}
50
- {video}`).description('图文/视频输出格式。<br/>可用占位符: `{title}`, `{cover}`, `{authorName}`, `{description}`, `{stats}`, `{sourceUrl}`, `{images}`, `{video}`, `{videoUrl}`'),
49
+ {video}`).description('图文/视频输出格式。<br/>可用占位符: `{title}`, `{cover}`, `{authorName}`, `{mainbody}`, `{stats}`, `{sourceUrl}`, `{video}`, `{videoUrl}`'),
51
50
  }).description("格式化模板"),
52
51
  koishi_1.Schema.object({
53
52
  parseLimit: koishi_1.Schema.number().default(3).description("单对话多链接解析上限"),
54
53
  useNumeral: koishi_1.Schema.boolean().default(true).description("使用格式化数字 (如 10000 -> 1万)"),
55
54
  showError: koishi_1.Schema.boolean().default(false).description("当链接不正确时提醒发送者"),
55
+ allow_sensitive: koishi_1.Schema.boolean().default(false).description("允许NSFW内容"),
56
+ proxy: koishi_1.Schema.string().description("代理设置"),
56
57
  }).description("高级解析设置"),
58
+ koishi_1.Schema.object({
59
+ onebotReadDir: koishi_1.Schema.string().description('OneBot 实现 (如 NapCat) 所在的容器或环境提供的路径前缀。').default("/app/.config/QQ/NapCat/temp"),
60
+ localDownloadDir: koishi_1.Schema.string().description('与上述路径对应的、Koishi 所在的容器或主机可以访问的路径前缀。').default("/koishi/data/temp"),
61
+ }).description('跨环境路径映射设置'),
57
62
  koishi_1.Schema.object({
58
63
  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"),
59
64
  logLevel: koishi_1.Schema.union([
@@ -93,7 +98,7 @@ function apply(ctx, config) {
93
98
  const now = Date.now();
94
99
  if (!lastProcessedUrls[channelId])
95
100
  lastProcessedUrls[channelId] = {};
96
- if (now - (lastProcessedUrls[channelId][link.url] || 0) < config.MinimumTimeInterval * 1000) {
101
+ if (now - (lastProcessedUrls[channelId][link.url] || 0) < config.Min_Interval * 1000) {
97
102
  if (config.logLevel === 'full')
98
103
  logger.info(`链接 ${link.url} 在冷却时间内,跳过处理。`);
99
104
  continue;
@@ -119,14 +124,14 @@ async function sendResult(session, config, result, logger) {
119
124
  return;
120
125
  }
121
126
  switch (config.useForward) {
122
- case 'forward':
123
- await (0, utils_1.sendResult_forward)(session, config, result, logger);
124
- return;
125
127
  case "plain":
126
128
  await (0, utils_1.sendResult_plain)(session, config, result, logger);
127
129
  return;
130
+ case 'forward':
131
+ await (0, utils_1.sendResult_forward)(session, config, result, logger, false);
132
+ return;
128
133
  case "mixed":
129
- await (0, utils_1.sendResult_mixed)(session, config, result, logger);
134
+ await (0, utils_1.sendResult_forward)(session, config, result, logger, true);
130
135
  return;
131
136
  }
132
137
  }
@@ -180,10 +180,9 @@ async function process(ctx, config, link, session) {
180
180
  platform: 'bilibili',
181
181
  title: data.title,
182
182
  authorName: data.owner.name,
183
- description: data.desc,
183
+ mainbody: (0, utils_1.escapeHtml)(data.desc),
184
184
  coverUrl: data.pic,
185
185
  videoUrl: videoUrl,
186
- duration: data.duration,
187
186
  sourceUrl: `https://www.bilibili.com/video/${data.bvid}`,
188
187
  stats: statsString,
189
188
  };
@@ -0,0 +1,46 @@
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
+ }
@@ -0,0 +1,10 @@
1
+ import { Context, Session } from 'koishi';
2
+ import { PluginConfig, ParsedInfo, Link } from '../types';
3
+ /**
4
+ * 在文本中匹配 Twitter/X 链接
5
+ */
6
+ export declare function match(content: string): Link[];
7
+ /**
8
+ * 处理单条 Twitter 链接
9
+ */
10
+ export declare function process(ctx: Context, config: PluginConfig, link: Link, session: Session): Promise<ParsedInfo | null>;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ // src/parsers/twitter.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.match = match;
5
+ exports.process = process;
6
+ const koishi_1 = require("koishi");
7
+ const utils_1 = require("../utils");
8
+ // ======================
9
+ // 链接匹配规则
10
+ // ======================
11
+ const linkRules = [
12
+ {
13
+ pattern: /https?:\/\/(?:www\.)?(?:twitter\.com|x\.com|mobile\.twitter\.com)\/([\w-]+)\/status\/(\d+)/gi,
14
+ type: "tweet",
15
+ },
16
+ {
17
+ pattern: /https?:\/\/t\.co\/([a-zA-Z0-9]+)/gi,
18
+ type: "short",
19
+ }
20
+ ];
21
+ /**
22
+ * 在文本中匹配 Twitter/X 链接
23
+ */
24
+ function match(content) {
25
+ const results = [];
26
+ const seen = new Set();
27
+ for (const rule of linkRules) {
28
+ let match;
29
+ while ((match = rule.pattern.exec(content)) !== null) {
30
+ const username = match[1] || 'unknown';
31
+ const tweetId = match[2] || match[1];
32
+ let cleanUrl = match[0];
33
+ // 处理短链接
34
+ if (rule.type === 'short') {
35
+ cleanUrl = `https://t.co/${tweetId}`;
36
+ // 标准化域名
37
+ }
38
+ else {
39
+ cleanUrl = `https://x.com/${username}/status/${tweetId}`;
40
+ }
41
+ if (seen.has(cleanUrl))
42
+ continue;
43
+ seen.add(cleanUrl);
44
+ results.push({
45
+ platform: 'twitter',
46
+ type: rule.type,
47
+ id: tweetId,
48
+ url: cleanUrl,
49
+ });
50
+ }
51
+ }
52
+ return results;
53
+ }
54
+ /**
55
+ * 处理单条 Twitter 链接
56
+ */
57
+ async function process(ctx, config, link, session) {
58
+ const logger = ctx.logger('twitter:process');
59
+ let apiUrl;
60
+ if (link.type === 'short') {
61
+ // 短链接需先解析,但 vxtwitter 支持直接转换
62
+ apiUrl = `https://api.vxtwitter.com/tweet?id=${link.id}`;
63
+ }
64
+ // 标准推文链接
65
+ apiUrl = link.url.replace("x.com", "api.vxtwitter.com");
66
+ if (!apiUrl) {
67
+ logger.warn(`无效的 Twitter 链接: ${link.url}`);
68
+ await session.send('无法解析此 Twitter 链接,URL 格式不正确');
69
+ return null;
70
+ }
71
+ try {
72
+ logger.info(`🔍 解析推文: ${apiUrl}`);
73
+ const tweetData = await ctx.http.get(apiUrl, {
74
+ headers: {
75
+ 'User-Agent': config.userAgent,
76
+ 'Accept': 'application/json'
77
+ }
78
+ });
79
+ // 处理 API 错误
80
+ if (tweetData?.error) {
81
+ logger.error(`API 错误: ${tweetData.error}`);
82
+ await session.send(`Twitter API 错误: ${tweetData.error}`);
83
+ return null;
84
+ }
85
+ if (tweetData.possibly_sensitive && !config.allow_sensitive) {
86
+ await session.send('潜在的不合规内容,已停止发送');
87
+ return null;
88
+ }
89
+ // 解析媒体
90
+ let media;
91
+ if (tweetData?.hasMedia) {
92
+ media = parseMedia(tweetData);
93
+ }
94
+ const likes = (0, utils_1.numeral)(parseInt(tweetData.likes), config);
95
+ const replies = (0, utils_1.numeral)(parseInt(tweetData.replies), config);
96
+ const retweets = (0, utils_1.numeral)(parseInt(tweetData.retweets), config);
97
+ const statsString = `点赞: ${likes} | 评论: ${replies} | 转发: ${retweets}`;
98
+ let tweet_text;
99
+ if (tweetData.replyingTo) {
100
+ tweet_text = "回复:" + tweetData.text;
101
+ }
102
+ else {
103
+ tweet_text = tweetData.text;
104
+ }
105
+ const image = media?.images ? media?.images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
106
+ const mainbody = (0, utils_1.escapeHtml)(tweet_text) + image;
107
+ return {
108
+ platform: 'twitter',
109
+ title: `@${tweetData.user_screen_name} 的推文`,
110
+ authorName: tweetData.user_name || tweetData.user_screen_name,
111
+ mainbody: mainbody,
112
+ sourceUrl: link.url,
113
+ stats: statsString,
114
+ videoUrl: media?.videos[0]?.url, // 取第一个视频
115
+ coverUrl: media?.videos[0]?.preview_url,
116
+ };
117
+ }
118
+ catch (error) {
119
+ logger.error(`解析失败: ${error.message || error}`);
120
+ // 专项错误处理
121
+ if (error.message?.includes('429')) {
122
+ await session.send('Twitter API 速率限制,请稍后再试');
123
+ }
124
+ else if (error.message?.includes('404')) {
125
+ await session.send('推文不存在或已删除');
126
+ }
127
+ else if (error.message?.includes('ECONNRESET') || error.message?.includes('ETIMEDOUT')) {
128
+ await session.send('连接 Twitter API 超时,请重试');
129
+ }
130
+ else {
131
+ await session.send(`解析失败: ${error.message}`);
132
+ }
133
+ return null;
134
+ }
135
+ }
136
+ // ======================
137
+ // 内部工具函数
138
+ // ======================
139
+ /**
140
+ * 解析媒体数据
141
+ */
142
+ function parseMedia(tweetData) {
143
+ const images = [];
144
+ const videos = [];
145
+ for (const media of tweetData.media_extended) {
146
+ switch (media.type) {
147
+ case 'image':
148
+ images.push(media.url);
149
+ continue;
150
+ case 'video':
151
+ videos.push({
152
+ url: media.url,
153
+ preview_url: media.thumbnail_url,
154
+ duration: media.duration_millis ? media.duration_millis / 1000 : undefined
155
+ });
156
+ continue;
157
+ }
158
+ }
159
+ return { images, videos };
160
+ }
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.match = match;
5
5
  exports.init = init;
6
6
  exports.process = process;
7
+ const koishi_1 = require("koishi");
7
8
  const cheerio_1 = require("cheerio");
8
9
  const utils_1 = require("../utils");
9
10
  const linkRules = [
@@ -260,17 +261,17 @@ async function process(ctx, config, link, session) {
260
261
  const collected = (0, utils_1.numeral)(parseInt(noteData.interactInfo.collectedCount), config);
261
262
  const comment = (0, utils_1.numeral)(parseInt(noteData.interactInfo.commentCount), config);
262
263
  const statsString = `点赞: ${liked} | 收藏: ${collected} | 评论: ${comment}`;
264
+ const image = images ? images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
265
+ const mainbody = (0, utils_1.escapeHtml)(noteData.desc.trim()) + image;
263
266
  return {
264
267
  platform: 'xiaohongshu',
265
268
  title: noteData.title,
266
269
  authorName: noteData.user.nickname,
267
- description: noteData.desc.trim(),
270
+ mainbody: mainbody,
268
271
  coverUrl: coverUrl,
269
272
  videoUrl: videoUrl,
270
- duration: (videoUrl && noteData.video?.media?.duration) ? noteData.video.media.duration / 1000 : null,
271
273
  sourceUrl: urlToFetch,
272
274
  stats: statsString,
273
- images: images.length > 0 ? images : undefined,
274
275
  };
275
276
  }
276
277
  catch (error) {
package/lib/types.d.ts CHANGED
@@ -8,25 +8,27 @@ export interface ParsedInfo {
8
8
  platform: string;
9
9
  title: string;
10
10
  authorName: string;
11
- description?: string;
11
+ mainbody?: string;
12
12
  coverUrl?: string;
13
13
  videoUrl?: string | null;
14
- duration?: number | null;
15
14
  sourceUrl: string;
16
15
  stats: string;
17
- images?: string[];
18
16
  }
19
17
  export interface PluginConfig {
20
18
  Video_ClarityPriority: '1' | '2';
21
- Maximumduration: number;
22
- Maximumduration_tip: string;
23
- MinimumTimeInterval: number;
19
+ Max_size: number;
20
+ Max_size_tip: string;
21
+ Min_Interval: number;
24
22
  waitTip_Switch: false | string;
25
23
  useForward: 'plain' | 'forward' | 'mixed';
26
24
  format: string;
27
25
  parseLimit: number;
28
26
  useNumeral: boolean;
29
27
  showError: boolean;
28
+ allow_sensitive: boolean;
29
+ proxy: string;
30
+ onebotReadDir: string;
31
+ localDownloadDir: string;
30
32
  userAgent: string;
31
33
  logLevel: 'none' | 'link_only' | 'full';
32
34
  }
package/lib/utils.d.ts CHANGED
@@ -7,6 +7,7 @@ import { Logger, Session } from "koishi";
7
7
  * @returns 格式化后的字符串
8
8
  */
9
9
  export declare function numeral(num: number, config: PluginConfig): string;
10
+ export declare function escapeHtml(str: string): string;
11
+ export declare function getFileSize(url: string, proxy: string | undefined, userAgent: string | undefined): Promise<number | null>;
10
12
  export declare function sendResult_plain(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger): Promise<void>;
11
- export declare function sendResult_forward(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger): Promise<void>;
12
- export declare function sendResult_mixed(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger): Promise<void>;
13
+ export declare function sendResult_forward(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger, mixed_sending?: boolean): Promise<void>;
package/lib/utils.js CHANGED
@@ -1,10 +1,55 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
2
38
  Object.defineProperty(exports, "__esModule", { value: true });
3
39
  exports.numeral = numeral;
40
+ exports.escapeHtml = escapeHtml;
41
+ exports.getFileSize = getFileSize;
4
42
  exports.sendResult_plain = sendResult_plain;
5
43
  exports.sendResult_forward = sendResult_forward;
6
- exports.sendResult_mixed = sendResult_mixed;
7
44
  const koishi_1 = require("koishi");
45
+ const path_1 = __importDefault(require("path"));
46
+ const fs_1 = require("fs");
47
+ const util_1 = require("util");
48
+ const stream_1 = require("stream");
49
+ const url_1 = require("url");
50
+ const http_proxy_agent_1 = require("http-proxy-agent");
51
+ const https_proxy_agent_1 = require("https-proxy-agent");
52
+ const fs = __importStar(require("node:fs"));
8
53
  /**
9
54
  * 将数字格式化为易读的字符串(如 万、亿)
10
55
  * @param num 数字
@@ -31,42 +76,239 @@ function escapeHtml(str) {
31
76
  .replace(/"/g, '&quot;')
32
77
  .replace(/'/g, '&#39;');
33
78
  }
79
+ function getProxyAgent(proxy, url) {
80
+ if (!proxy)
81
+ return undefined;
82
+ const u = new url_1.URL(url);
83
+ if (u.protocol === 'https:') {
84
+ return new https_proxy_agent_1.HttpsProxyAgent(proxy);
85
+ }
86
+ else {
87
+ return new http_proxy_agent_1.HttpProxyAgent(proxy);
88
+ }
89
+ }
90
+ function parseHtmlToSegments(html) {
91
+ const segments = [];
92
+ // 匹配 <img> 标签和普通文本
93
+ const tokens = html.split(/(<img\s[^>]*src\s*=\s*["']?[^"'>\s]+["']?[^>]*>)/gi);
94
+ for (const token of tokens) {
95
+ if (!token)
96
+ continue;
97
+ const imgMatch = token.match(/<img\s[^>]*src\s*=\s*["']?([^"'>\s]+)["']?/i);
98
+ if (imgMatch) {
99
+ const url = imgMatch[1];
100
+ segments.push({ type: 'image', data: { file: url } });
101
+ }
102
+ else {
103
+ // 非 <img> 部分:当作普通文本(已转义,可直接使用)
104
+ // 注意:HTML 中的换行可能是 <br> 或 \n,根据实际情况处理
105
+ // 此处假设换行用 \n 表示(或上游已转换)
106
+ if (token.trim() !== '') {
107
+ segments.push({ type: 'text', data: { text: token } });
108
+ }
109
+ }
110
+ }
111
+ return segments;
112
+ }
113
+ async function downloadAndMapUrl(url, proxy, userAgent, localDownloadDir, onebotReadDir, logger) {
114
+ await fs.promises.mkdir(localDownloadDir, { recursive: true });
115
+ const u = new url_1.URL(url);
116
+ const ext = path_1.default.extname(u.pathname).split('?')[0] || '.bin';
117
+ const safeFilename = `${Date.now()}_${Math.random().toString(36).substring(2, 10)}${ext}`;
118
+ const actualPath = path_1.default.join(localDownloadDir, safeFilename);
119
+ const onebotPath = path_1.default.posix.join(onebotReadDir, safeFilename);
120
+ const fileUrl = `file://${onebotPath}`;
121
+ return new Promise((resolve, reject) => {
122
+ const agent = getProxyAgent(proxy, url);
123
+ const headers = {
124
+ 'User-Agent': userAgent,
125
+ 'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
126
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
127
+ 'Connection': 'keep-alive'
128
+ };
129
+ const getter = u.protocol === 'https:' ? require('https').get : require('http').get;
130
+ const req = getter(url, { agent, timeout: 30000, headers }, (res) => {
131
+ // 处理重定向
132
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
133
+ req.destroy();
134
+ logger.debug(`重定向: ${url} -> ${res.headers.location}`);
135
+ downloadAndMapUrl(res.headers.location, proxy, userAgent, localDownloadDir, onebotReadDir, logger)
136
+ .then(resolve)
137
+ .catch(reject);
138
+ return;
139
+ }
140
+ if (res.statusCode !== 200) {
141
+ req.destroy();
142
+ reject(new Error(`HTTP ${res.statusCode} when fetching ${url}`));
143
+ return;
144
+ }
145
+ // 检查内容类型,避免下载非图片内容
146
+ const contentType = res.headers['content-type'] || '';
147
+ if (!contentType.startsWith('image/') && !contentType.includes('video/')) {
148
+ req.destroy();
149
+ reject(new Error(`Unexpected content type: ${contentType}`));
150
+ return;
151
+ }
152
+ const pipelineAsync = (0, util_1.promisify)(stream_1.pipeline);
153
+ pipelineAsync(res, (0, fs_1.createWriteStream)(actualPath))
154
+ .then(() => {
155
+ logger.debug(`下载成功: ${url} -> ${fileUrl}`);
156
+ resolve(fileUrl);
157
+ })
158
+ .catch((err) => {
159
+ req.destroy();
160
+ reject(new Error(`Pipeline failed: ${err.message}`));
161
+ });
162
+ });
163
+ req.on('error', (err) => {
164
+ req.destroy();
165
+ reject(new Error(`Request error: ${err.message}`));
166
+ });
167
+ req.on('timeout', () => {
168
+ req.destroy();
169
+ reject(new Error('Request timeout'));
170
+ });
171
+ });
172
+ }
173
+ async function getFileSize(url, proxy, userAgent) {
174
+ return new Promise((resolve) => {
175
+ const u = new url_1.URL(url);
176
+ const agent = getProxyAgent(proxy, url);
177
+ const getter = u.protocol === 'https:' ? require('https').get : require('http').get;
178
+ const req = getter(url, {
179
+ agent,
180
+ method: 'HEAD',
181
+ timeout: 10000,
182
+ headers: {
183
+ 'User-Agent': userAgent
184
+ }
185
+ }, (res) => {
186
+ const len = res.headers['content-length'];
187
+ if (len && /^\d+$/.test(len)) {
188
+ resolve(parseInt(len, 10));
189
+ }
190
+ else {
191
+ resolve(null);
192
+ }
193
+ req.destroy();
194
+ });
195
+ req.on('error', () => {
196
+ req.destroy();
197
+ resolve(null);
198
+ });
199
+ req.on('timeout', () => {
200
+ req.destroy();
201
+ resolve(null);
202
+ });
203
+ });
204
+ }
34
205
  async function sendResult_plain(session, config, result, logger) {
35
206
  if (config.logLevel === 'full') {
36
- logger.info('进入普通消息发送');
207
+ logger.info('进入普通发送');
208
+ }
209
+ const localDownloadDir = config.localDownloadDir;
210
+ const onebotReadDir = config.onebotReadDir;
211
+ let mediaCoverUrl = result.coverUrl;
212
+ let mediaVideoUrl = result.videoUrl || null;
213
+ let mediaMainbody = result.mainbody;
214
+ // --- 下载封面 ---
215
+ if (result.coverUrl) {
216
+ try {
217
+ mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
218
+ if (config.logLevel === 'full')
219
+ logger.info(`封面已下载: ${mediaCoverUrl}`);
220
+ }
221
+ catch (e) {
222
+ logger.warn(`封面下载失败: ${result.coverUrl}`, e);
223
+ mediaCoverUrl = '';
224
+ }
225
+ }
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
+ // --- 下载 mainbody 中的图片 ---
262
+ if (result.mainbody) {
263
+ const imgMatches = [...result.mainbody.matchAll(/<img\s[^>]*src\s*=\s*["']?([^"'>\s]+)["']?/gi)];
264
+ const urlMap = {};
265
+ await Promise.all(imgMatches.map(async (match) => {
266
+ const remoteUrl = match[1];
267
+ try {
268
+ const localUrl = await downloadAndMapUrl(remoteUrl, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
269
+ urlMap[remoteUrl] = localUrl;
270
+ if (config.logLevel === 'full')
271
+ logger.info(`正文图片已下载: ${localUrl}`);
272
+ }
273
+ catch (e) {
274
+ logger.warn(`正文图片下载失败: ${remoteUrl}`, e);
275
+ }
276
+ }));
277
+ mediaMainbody = result.mainbody;
278
+ for (const [remote, local] of Object.entries(urlMap)) {
279
+ const escaped = remote.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
280
+ mediaMainbody = mediaMainbody.replace(new RegExp(escaped, 'g'), local);
281
+ }
37
282
  }
283
+ // === 模板替换 ===
38
284
  let message = config.format;
39
- // 对所有文本内容进行 HTML 转义
40
285
  message = message.replace(/{title}/g, escapeHtml(result.title || ''));
41
286
  message = message.replace(/{authorName}/g, escapeHtml(result.authorName || ''));
42
- message = message.replace(/{description}/g, escapeHtml(result.description ? result.description : ''));
287
+ message = message.replace(/{mainbody}/g, mediaMainbody ?? '');
43
288
  message = message.replace(/{sourceUrl}/g, escapeHtml(result.sourceUrl || ''));
44
- message = message.replace(/{cover}/g, result.coverUrl ? koishi_1.h.image(result.coverUrl).toString() : '');
45
- const imagesText = result.images ? result.images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
46
- message = message.replace(/{images}/g, imagesText);
289
+ message = message.replace(/{cover}/g, mediaCoverUrl ? koishi_1.h.image(mediaCoverUrl).toString() : '');
47
290
  message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
48
- // 只要 videoUrl 存在就处理,仅当 duration 明确超长时才替换为提示
291
+ // 处理视频相关占位符
49
292
  if (result.videoUrl) {
50
293
  message = message.replace(/{videoUrl}/g, escapeHtml(result.videoUrl));
51
- // 仅当 duration 是有效数字且超长时,才显示提示
52
- if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
53
- const tip = escapeHtml(config.Maximumduration_tip || '');
294
+ if (videoExceedsLimit) {
295
+ const tip = escapeHtml(config.Max_size_tip);
54
296
  message = message.replace(/{video}/g, tip);
55
297
  }
298
+ else if (mediaVideoUrl) {
299
+ message = message.replace(/{video}/g, koishi_1.h.video(mediaVideoUrl).toString());
300
+ }
56
301
  else {
57
- // 正常发送视频和链接
58
- message = message.replace(/{video}/g, koishi_1.h.video(result.videoUrl).toString());
302
+ message = message.replace(/{video}/g, '');
59
303
  }
60
304
  if (config.logLevel === 'link_only') {
61
305
  logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
62
306
  }
63
307
  }
64
308
  else {
65
- // 没有视频则移除占位符
66
309
  message = message.replace(/{video}/g, '');
67
310
  message = message.replace(/{videoUrl}/g, '');
68
311
  }
69
- // 过滤空行,保留含有 < 的行(如图片、视频标签)
70
312
  const cleanMessage = message.split('\n').filter(line => line.trim() !== '' || line.includes('<')).join('\n');
71
313
  if (config.logLevel === 'full') {
72
314
  logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
@@ -75,201 +317,138 @@ async function sendResult_plain(session, config, result, logger) {
75
317
  await session.send(koishi_1.h.quote(session.messageId) + cleanMessage);
76
318
  }
77
319
  }
78
- async function sendResult_forward(session, config, result, logger) {
320
+ async function sendResult_forward(session, config, result, logger, mixed_sending = false) {
79
321
  if (config.logLevel === 'full') {
80
- logger.info('进入合并转发发送');
81
- }
82
- let message = config.format;
83
- // Step 1: 替换纯文本字段
84
- message = message.replace(/{title}/g, escapeHtml(result.title || ''));
85
- message = message.replace(/{authorName}/g, escapeHtml(result.authorName || ''));
86
- message = message.replace(/{description}/g, escapeHtml(result.description || ''));
87
- message = message.replace(/{sourceUrl}/g, escapeHtml(result.sourceUrl || ''));
88
- message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
89
- if (result.videoUrl) {
90
- message = message.replace(/{videoUrl}/g, escapeHtml(result.videoUrl));
91
- }
92
- if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
93
- const tip = escapeHtml(config.Maximumduration_tip || '');
94
- message = message.replace(/{video}/g, tip);
322
+ logger.info(mixed_sending ? '进入混合发送' : '进入合并发送');
95
323
  }
96
- // Step 2: 检查是否包含视频占位符
97
- const hasVideoInTemplate = message.includes('{video}');
98
- // Step 3: 构建富媒体映射
99
- const mediaMap = {};
100
- // 处理封面
324
+ const localDownloadDir = config.localDownloadDir;
325
+ const onebotReadDir = config.onebotReadDir;
326
+ let mediaCoverUrl = result.coverUrl;
327
+ let mediaVideoUrl = result.videoUrl || null;
328
+ let mediaMainbody = result.mainbody;
329
+ // --- 封面 ---
101
330
  if (result.coverUrl) {
102
- mediaMap['{cover}'] = [{ type: 'image', data: { file: result.coverUrl } }];
103
- }
104
- else {
105
- mediaMap['{cover}'] = [];
106
- }
107
- // 处理图片列表
108
- if (result.images && result.images.length > 0) {
109
- mediaMap['{images}'] = result.images.map(img => ({ type: 'image', data: { file: img } }));
110
- }
111
- else {
112
- mediaMap['{images}'] = [];
113
- }
114
- // Step 4: 按行处理,仅过滤纯空行,并精确控制换行
115
- const lines = message.split('\n').filter(line => line.trim() !== '');
116
- const nonVideoSegments = [];
117
- for (let i = 0; i < lines.length; i++) {
118
- const line = lines[i];
119
- const isLastLine = i === lines.length - 1;
120
- // 按富媒体占位符分割
121
- const tokens = line.split(/(\{cover\}|\{images\}|\{video\})/g);
122
- // 用于存储当前行的消息段
123
- const currentLineSegments = [];
124
- let hasTextContent = false; // 新增标志:当前行是否包含纯文本
125
- for (const token of tokens) {
126
- if (token === '{cover}' || token === '{images}') {
127
- // 插入对应的消息段
128
- currentLineSegments.push(...mediaMap[token]);
129
- }
130
- else if (token === '{video}') {
131
- // 视频不放入 nonVideoSegments,跳过
132
- }
133
- else if (token.trim() !== '') {
134
- // 普通文本
135
- currentLineSegments.push({ type: 'text', data: { text: token } });
136
- hasTextContent = true; // 标记当前行有文本
137
- }
138
- // 注意:token 为空字符串时(如占位符在行首/尾),不添加任何内容
139
- }
140
- // 只有当 currentLineSegments 不为空时,才将其加入总列表
141
- if (currentLineSegments.length > 0) {
142
- nonVideoSegments.push(...currentLineSegments);
331
+ try {
332
+ mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
143
333
  }
144
- // 如果不是最后一行,且当前行非空,则添加一个换行符
145
- if (!isLastLine && hasTextContent) {
146
- nonVideoSegments.push({ type: 'text', data: { text: '\n' } });
334
+ catch (e) {
335
+ logger.warn('封面下载失败', e);
336
+ mediaCoverUrl = '';
147
337
  }
148
338
  }
149
- // Step 5: 构建转发节点
150
- const forwardNodes = [];
151
- // 非视频内容节点
152
- if (nonVideoSegments.length > 0) {
153
- forwardNodes.push({
154
- type: 'node',
155
- data: {
156
- user_id: session.selfId,
157
- nickname: '分享助手',
158
- content: nonVideoSegments
159
- }
160
- });
161
- }
162
- // 视频节点(仅当模板中有 {video} 且有有效视频时)
163
- if (hasVideoInTemplate && result.videoUrl) {
164
- if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
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},默认允许下载`);
165
347
  }
166
348
  else {
167
- forwardNodes.push({
168
- type: 'node',
169
- data: {
170
- user_id: session.selfId,
171
- nickname: '分享助手',
172
- content: [
173
- { type: 'video', data: { file: result.videoUrl } },
174
- ]
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}`);
175
356
  }
176
- });
177
- }
178
- if (config.logLevel === 'link_only') {
179
- logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
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
+ }
180
370
  }
181
371
  }
182
- if (forwardNodes.length === 0)
183
- return;
184
- if (config.logLevel === 'full') {
185
- logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
186
- }
187
- // Step 6: 发送合并转发
188
- if (!(session.onebot && session.onebot._request))
189
- throw new Error("Onebot is not defined");
190
- await session.onebot._request('send_group_forward_msg', {
191
- group_id: session.guildId,
192
- messages: forwardNodes,
193
- news: [{ text: result.description || '-' }, { text: '点击查看详情 | Powered by furryaxw' }],
194
- prompt: result.title || '',
195
- summary: '分享解析',
196
- source: result.title || ''
197
- });
198
- }
199
- async function sendResult_mixed(session, config, result, logger) {
200
- if (config.logLevel === 'full') {
201
- logger.info('进入混合转发发送');
372
+ // --- mainbody 图片 ---
373
+ if (result.mainbody) {
374
+ const imgUrls = [...result.mainbody.matchAll(/<img\s[^>]*src\s*=\s*["']?([^"'>\s]+)["']?/gi)].map(m => m[1]);
375
+ const urlMap = {};
376
+ await Promise.all(imgUrls.map(async (url) => {
377
+ try {
378
+ urlMap[url] = await downloadAndMapUrl(url, config.proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
379
+ }
380
+ catch (e) {
381
+ logger.warn(`正文图片下载失败: ${url}`, e);
382
+ }
383
+ }));
384
+ mediaMainbody = result.mainbody;
385
+ for (const [remote, local] of Object.entries(urlMap)) {
386
+ const escaped = remote.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
387
+ mediaMainbody = mediaMainbody.replace(new RegExp(escaped, 'g'), local);
388
+ }
202
389
  }
390
+ // === 构建消息模板 ===
203
391
  let message = config.format;
204
- // Step 1: 替换纯文本字段
205
392
  message = message.replace(/{title}/g, escapeHtml(result.title || ''));
206
393
  message = message.replace(/{authorName}/g, escapeHtml(result.authorName || ''));
207
- message = message.replace(/{description}/g, escapeHtml(result.description || ''));
208
394
  message = message.replace(/{sourceUrl}/g, escapeHtml(result.sourceUrl || ''));
209
395
  message = message.replace(/{stats}/g, escapeHtml(result.stats || ''));
396
+ // 处理 {videoUrl} 和 {video} 占位符逻辑(用于后续判断)
210
397
  if (result.videoUrl) {
211
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} 为实际视频,留到转发节点构建时处理
212
404
  }
213
- if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
214
- const tip = escapeHtml(config.Maximumduration_tip || '');
215
- message = message.replace(/{video}/g, tip);
216
- }
217
- // Step 2: 检查是否包含视频占位符
218
405
  const hasVideoInTemplate = message.includes('{video}');
219
- // Step 3: 构建富媒体映射
220
406
  const mediaMap = {};
221
- // 处理封面
222
- if (result.coverUrl) {
223
- mediaMap['{cover}'] = [{ type: 'image', data: { file: result.coverUrl } }];
407
+ if (mediaCoverUrl) {
408
+ mediaMap['{cover}'] = [{ type: 'image', data: { file: mediaCoverUrl } }];
224
409
  }
225
410
  else {
226
411
  mediaMap['{cover}'] = [];
227
412
  }
228
- // 处理图片列表
229
- if (result.images && result.images.length > 0) {
230
- mediaMap['{images}'] = result.images.map(img => ({ type: 'image', data: { file: img } }));
231
- }
232
- else {
233
- mediaMap['{images}'] = [];
234
- }
235
- // Step 4: 按行处理,仅过滤纯空行,并精确控制换行
236
413
  const lines = message.split('\n').filter(line => line.trim() !== '');
237
414
  const nonVideoSegments = [];
238
415
  for (let i = 0; i < lines.length; i++) {
239
416
  const line = lines[i];
240
417
  const isLastLine = i === lines.length - 1;
241
- // 按富媒体占位符分割
242
- const tokens = line.split(/(\{cover\}|\{images\}|\{video\})/g);
243
- // 用于存储当前行的消息段
418
+ const tokens = line.split(/(\{cover\}|\{video\})/g);
244
419
  const currentLineSegments = [];
245
- let hasTextContent = false; // 新增标志:当前行是否包含纯文本
420
+ let hasTextContent = false;
246
421
  for (const token of tokens) {
247
- if (token === '{cover}' || token === '{images}') {
248
- // 插入对应的消息段
422
+ if (token === '{cover}') {
249
423
  currentLineSegments.push(...mediaMap[token]);
250
424
  }
425
+ else if (token === '{mainbody}') {
426
+ const parsed = parseHtmlToSegments(mediaMainbody || '');
427
+ currentLineSegments.push(...parsed);
428
+ hasTextContent = parsed.some(seg => seg.type === 'text');
429
+ }
251
430
  else if (token === '{video}') {
252
- // 视频不放入 nonVideoSegments,跳过
431
+ // 超限时替换为提示文本;否则留空(由转发节点处理)
432
+ if (videoExceedsLimit) {
433
+ const tip = config.Max_size_tip;
434
+ currentLineSegments.push({ type: 'text', data: { text: tip } });
435
+ hasTextContent = true;
436
+ }
437
+ // 否则不插入内容(视频将作为独立节点)
253
438
  }
254
439
  else if (token.trim() !== '') {
255
- // 普通文本
256
440
  currentLineSegments.push({ type: 'text', data: { text: token } });
257
- hasTextContent = true; // 标记当前行有文本
441
+ hasTextContent = true;
258
442
  }
259
- // 注意:token 为空字符串时(如占位符在行首/尾),不添加任何内容
260
443
  }
261
- // 只有当 currentLineSegments 不为空时,才将其加入总列表
262
444
  if (currentLineSegments.length > 0) {
263
445
  nonVideoSegments.push(...currentLineSegments);
264
446
  }
265
- // 如果不是最后一行,且当前行非空,则添加一个换行符
266
447
  if (!isLastLine && hasTextContent) {
267
448
  nonVideoSegments.push({ type: 'text', data: { text: '\n' } });
268
449
  }
269
450
  }
270
- // Step 5: 构建转发节点
271
451
  const forwardNodes = [];
272
- // 非视频内容节点
273
452
  if (nonVideoSegments.length > 0) {
274
453
  forwardNodes.push({
275
454
  type: 'node',
@@ -280,13 +459,20 @@ async function sendResult_mixed(session, config, result, logger) {
280
459
  }
281
460
  });
282
461
  }
283
- let video;
284
- // 视频节点(仅当模板中有 {video} 且有有效视频时)
285
- if (hasVideoInTemplate && result.videoUrl) {
286
- if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
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 } }]
471
+ }
472
+ });
287
473
  }
288
474
  else {
289
- video = koishi_1.h.video(result.videoUrl).toString();
475
+ videoElement = koishi_1.h.video(mediaVideoUrl).toString();
290
476
  }
291
477
  if (config.logLevel === 'link_only') {
292
478
  logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
@@ -297,19 +483,19 @@ async function sendResult_mixed(session, config, result, logger) {
297
483
  if (config.logLevel === 'full') {
298
484
  logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
299
485
  }
300
- // Step 6: 发送合并转发
301
486
  if (!(session.onebot && session.onebot._request))
302
487
  throw new Error("Onebot is not defined");
303
488
  const promises = [];
304
489
  promises.push(session.onebot._request('send_group_forward_msg', {
305
490
  group_id: session.guildId,
306
491
  messages: forwardNodes,
307
- news: [{ text: result.description || '-' }, { text: '点击查看详情 | Powered by furryaxw' }],
492
+ news: [{ text: mediaMainbody || '-' }, { text: '点击查看详情 | Powered by furryaxw' }],
308
493
  prompt: result.title || '',
309
494
  summary: '分享解析',
310
495
  source: result.title || ''
311
496
  }));
312
- if (video)
313
- promises.push(session.send(video));
497
+ if (mixed_sending && videoElement) {
498
+ promises.push(session.send(videoElement));
499
+ }
314
500
  await Promise.all(promises);
315
501
  }
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.4.0",
5
+ "version": "0.5.0",
6
6
  "main": "lib/index.js",
7
7
  "typings": "lib/index.d.ts",
8
8
  "files": [