koishi-plugin-share-links-analysis 0.6.3 → 0.7.1

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/README.md CHANGED
@@ -10,8 +10,7 @@
10
10
 
11
11
  特别鸣谢以下项目的支持:
12
12
 
13
- - [@summonhim/koishi-plugin-bili-parser](https://www.npmjs.com/package/@summonhim/koishi-plugin-bili-parser)
14
- - [koishi-plugin-iirose-media-request](https://www.npmjs.com/package/koishi-plugin-iirose-media-request)
15
-
16
- 原项目:[koishi-plugin-bilibili-videolink-analysis](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/bilibili-videolink-analysis)
17
- ~~<br>本项目基于原项目0.6.3版本修改~~ 已重构大部分代码
13
+ - [koishi-plugin-bilibili-videolink-analysis](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/bilibili-videolink-analysis)
14
+ - [koishi-plugin-xiaohongshu](https://www.npmjs.com/package/koishi-plugin-xiaohongshu)
15
+ 以及解析方式原作者:[@MuJie](https://mu-jie.cc/)
16
+ - [BetterTwitFix](https://github.com/dylanpdx/BetterTwitFix)
package/lib/core.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, Session } from 'koishi';
2
2
  import { Link, PluginConfig, ParsedInfo } from './types';
3
- export declare const parsers_str: string[];
3
+ export declare const parsers_str: ("bilibili" | "xiaohongshu" | "twitter" | "xiaoheihe")[];
4
4
  /**
5
5
  * 从文本中解析出所有支持的链接
6
6
  * @param content 消息内容
package/lib/core.js CHANGED
@@ -41,9 +41,10 @@ exports.init = init;
41
41
  const Bilibili = __importStar(require("./parsers/bilibili"));
42
42
  const Xiaohongshu = __importStar(require("./parsers/xiaohongshu"));
43
43
  const Twitter = __importStar(require("./parsers/twitter"));
44
+ const Xiaoheihe = __importStar(require("./parsers/xiaoheihe"));
44
45
  // 定义所有支持的解析器
45
- const parsers = [Bilibili, Xiaohongshu, Twitter];
46
- exports.parsers_str = ['bilibili', 'xiaohongshu', 'twitter'];
46
+ const parsers = [Bilibili, Xiaohongshu, Twitter, Xiaoheihe];
47
+ exports.parsers_str = parsers.map(p => p.name);
47
48
  /**
48
49
  * 从文本中解析出所有支持的链接
49
50
  * @param content 消息内容
@@ -67,7 +68,9 @@ function resolveLinks(content) {
67
68
  */
68
69
  async function processLink(ctx, config, link, session) {
69
70
  for (const parser of parsers) {
70
- if (parser.match(link.url).length > 0) {
71
+ if (parser.name == link.platform) {
72
+ if (config.logLevel == "full")
73
+ ctx.logger('share-links-analysis').info(`解析平台:${parser.name},链接:${link.url}`);
71
74
  return await parser.process(ctx, config, link, session);
72
75
  }
73
76
  }
package/lib/index.js CHANGED
@@ -35,6 +35,7 @@ exports.Config = koishi_1.Schema.intersect([
35
35
  koishi_1.Schema.const("forward").description("合并转发"),
36
36
  koishi_1.Schema.const("mixed").description("混合发送"),
37
37
  ]).default("forward").description("发送模式"),
38
+ usingLocal: koishi_1.Schema.boolean().default(false).description("使用本地文件(关闭后代理设置无效)"),
38
39
  sendFiles: koishi_1.Schema.boolean().default(true).description("是否发送文件(视频等)"),
39
40
  sendLinks: koishi_1.Schema.boolean().default(false).description("是否附加直链(仅对合并发送有效)"),
40
41
  }).description("基础设置"),
@@ -51,7 +52,7 @@ exports.Config = koishi_1.Schema.intersect([
51
52
  koishi_1.Schema.object({
52
53
  parseLimit: koishi_1.Schema.number().default(3).description("单对话多链接解析上限"),
53
54
  useNumeral: koishi_1.Schema.boolean().default(true).description("使用格式化数字 (如 10000 -> 1万)"),
54
- showError: koishi_1.Schema.boolean().default(false).description("当链接不正确时提醒发送者"),
55
+ showError: koishi_1.Schema.boolean().default(false).description("当链接被阻止时提醒发送者"),
55
56
  }).description("高级解析设置"),
56
57
  koishi_1.Schema.object({
57
58
  proxy: koishi_1.Schema.string().description("代理设置"),
@@ -108,7 +109,8 @@ function apply(ctx, config) {
108
109
  if (!await (0, utils_1.isUserAdmin)(session, session.userId))
109
110
  return '权限不足';
110
111
  if (parser) {
111
- if (!core_1.parsers_str.includes(parser))
112
+ const isValidParser = (name) => core_1.parsers_str.includes(name);
113
+ if (!isValidParser(parser))
112
114
  return '请输入正确的解析器名称';
113
115
  if (!value)
114
116
  return '请输入正确的模式';
@@ -165,12 +167,22 @@ function apply(ctx, config) {
165
167
  for (const link of links) {
166
168
  if (session.guildId) {
167
169
  const settings = await (0, utils_1.getEffectiveSettings)(ctx, session.guildId, config);
168
- if (!settings.parsers[link.platform])
170
+ if (!settings.parsers[link.platform]) {
171
+ if (config.logLevel == "full")
172
+ ctx.logger('share-links-analysis').info(`根据策略,该链接已被阻止解析:平台:${link.platform},链接:${link.url}`);
173
+ if (config.showError)
174
+ await session.send(`根据策略,该链接已被阻止解析:平台:${link.platform}`);
169
175
  continue;
176
+ }
170
177
  }
171
178
  else {
172
- if (!config.default_parsers[link.platform])
179
+ if (!config.default_parsers[link.platform]) {
180
+ if (config.logLevel == "full")
181
+ ctx.logger('share-links-analysis').info(`根据策略,该链接已被阻止解析:平台:${link.platform},链接:${link.url}`);
182
+ if (config.showError)
183
+ await session.send(`根据策略,该链接已被阻止解析:平台:${link.platform}`);
173
184
  continue;
185
+ }
174
186
  }
175
187
  if (linkCount >= config.parseLimit) {
176
188
  await session.send("已达到单次解析上限…");
@@ -192,9 +204,6 @@ function apply(ctx, config) {
192
204
  lastProcessedUrls[channelId][link.url] = now;
193
205
  await sendResult(session, config, result, logger);
194
206
  }
195
- else if (config.showError) {
196
- await session.send(`无法解析链接:${link.url}。可能是不支持的类型或链接有误。`);
197
- }
198
207
  linkCount++;
199
208
  }
200
209
  });
@@ -1,5 +1,6 @@
1
1
  import { Context, Session } from 'koishi';
2
2
  import { Link, ParsedInfo, PluginConfig } from '../types';
3
+ export declare const name = "bilibili";
3
4
  /**
4
5
  * 在文本中匹配B站链接 (长链/短链/纯BV号)
5
6
  * @param content 消息内容
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  // src/parsers/bilibili.ts
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.name = void 0;
4
5
  exports.match = match;
5
6
  exports.process = process;
6
7
  const utils_1 = require("../utils");
8
+ exports.name = "bilibili";
7
9
  const linkRules = [
8
10
  {
9
11
  pattern: /(?:https?:\/\/)?(?:www\.bilibili\.com\/video\/)(([ab]v[0-9a-zA-Z]+))/gi,
@@ -36,7 +38,7 @@ function match(content) {
36
38
  continue;
37
39
  seen.add(url);
38
40
  results.push({
39
- platform: 'bilibili',
41
+ platform: exports.name,
40
42
  type,
41
43
  id,
42
44
  url,
@@ -52,7 +54,7 @@ function match(content) {
52
54
  continue;
53
55
  seen.add(url);
54
56
  results.push({
55
- platform: 'bilibili',
57
+ platform: exports.name,
56
58
  type: 'video',
57
59
  id: videoId,
58
60
  url,
@@ -69,7 +71,7 @@ function match(content) {
69
71
  * @returns 处理后的标准格式对象
70
72
  */
71
73
  async function process(ctx, config, link, session) {
72
- const logger = ctx.logger('share-links-analysis:bilibili');
74
+ const logger = ctx.logger(`share-links-analysis:${exports.name}`);
73
75
  let videoId = link.id;
74
76
  let videoIdType = link.type;
75
77
  if (link.type === 'short') {
@@ -180,7 +182,7 @@ async function process(ctx, config, link, session) {
180
182
  files = [{ type: "video", url: videoUrl }];
181
183
  }
182
184
  return {
183
- platform: 'bilibili',
185
+ platform: exports.name,
184
186
  title: data.title,
185
187
  authorName: data.owner.name,
186
188
  mainbody: (0, utils_1.escapeHtml)(data.desc),
@@ -1,5 +1,6 @@
1
1
  import { Context, Session } from 'koishi';
2
2
  import { PluginConfig, ParsedInfo, Link } from '../types';
3
+ export declare const name = "twitter";
3
4
  /**
4
5
  * 在文本中匹配 Twitter/X 链接
5
6
  */
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  // src/parsers/twitter.ts
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.name = void 0;
4
5
  exports.match = match;
5
6
  exports.process = process;
6
7
  const koishi_1 = require("koishi");
7
8
  const utils_1 = require("../utils");
8
- // ======================
9
- // 链接匹配规则
10
- // ======================
9
+ exports.name = "twitter";
11
10
  const linkRules = [
12
11
  {
13
12
  pattern: /https?:\/\/(?:www\.)?(?:twitter\.com|x\.com|mobile\.twitter\.com)\/([\w-]+)\/status\/(\d+)/gi,
@@ -42,7 +41,7 @@ function match(content) {
42
41
  continue;
43
42
  seen.add(cleanUrl);
44
43
  results.push({
45
- platform: 'twitter',
44
+ platform: exports.name,
46
45
  type: rule.type,
47
46
  id: tweetId,
48
47
  url: cleanUrl,
@@ -55,7 +54,7 @@ function match(content) {
55
54
  * 处理单条 Twitter 链接
56
55
  */
57
56
  async function process(ctx, config, link, session) {
58
- const logger = ctx.logger('twitter:process');
57
+ const logger = ctx.logger(`share-links-analysis:${exports.name}`);
59
58
  let apiUrl;
60
59
  if (link.type === 'short') {
61
60
  // 短链接需先解析,但 vxtwitter 支持直接转换
@@ -84,7 +83,8 @@ async function process(ctx, config, link, session) {
84
83
  }
85
84
  const enable_nsfw = await (0, utils_1.getEffectiveSettings)(ctx, session.guildId, config);
86
85
  if (tweetData.possibly_sensitive && !enable_nsfw) {
87
- await session.send('潜在的不合规内容,已停止发送');
86
+ if (config.showError)
87
+ await session.send(`潜在的不合规内容,根据策略已停止发送`);
88
88
  return null;
89
89
  }
90
90
  // 解析媒体
@@ -113,7 +113,7 @@ async function process(ctx, config, link, session) {
113
113
  }
114
114
  }
115
115
  return {
116
- platform: 'twitter',
116
+ platform: exports.name,
117
117
  title: `@${tweetData.user_screen_name} 的推文`,
118
118
  authorName: tweetData.user_name || tweetData.user_screen_name,
119
119
  mainbody: mainbody,
@@ -161,7 +161,6 @@ function parseMedia(tweetData) {
161
161
  preview_url: media.thumbnail_url,
162
162
  duration: media.duration_millis ? media.duration_millis / 1000 : undefined
163
163
  });
164
- continue;
165
164
  }
166
165
  }
167
166
  return { images, videos };
@@ -0,0 +1,5 @@
1
+ import { Context, Session } from 'koishi';
2
+ import { PluginConfig, ParsedInfo, Link } from '../types';
3
+ export declare const name = "xiaoheihe";
4
+ export declare function match(content: string): Link[];
5
+ export declare function process(ctx: Context, config: PluginConfig, link: Link, session: Session): Promise<ParsedInfo | null>;
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+ // src/parsers/xiaoheihe.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.name = void 0;
5
+ exports.match = match;
6
+ exports.process = process;
7
+ const koishi_1 = require("koishi");
8
+ const utils_1 = require("../utils");
9
+ exports.name = "xiaoheihe";
10
+ const linkRules = [
11
+ {
12
+ pattern: /https?:\/\/api.xiaoheihe\.cn\/v3\/bbs\/app\/api\/web\/share\?link_id=\w+/gi,
13
+ type: "bbs_api",
14
+ },
15
+ {
16
+ pattern: /https?:\/\/www.xiaoheihe\.cn\/app\/bbs\/link\/\w+/gi,
17
+ type: "bbs",
18
+ }
19
+ ];
20
+ function match(content) {
21
+ const results = [];
22
+ for (const rule of linkRules) {
23
+ const match = content.match(rule.pattern);
24
+ if (match) {
25
+ for (const fullUrl of match) {
26
+ const id = fullUrl.match(/\w+$/)?.[0];
27
+ if (id) {
28
+ results.push({
29
+ platform: exports.name,
30
+ type: rule.type,
31
+ id,
32
+ url: `https://www.xiaoheihe.cn/app/bbs/link/${id}`,
33
+ });
34
+ }
35
+ }
36
+ }
37
+ }
38
+ return results;
39
+ }
40
+ async function process(ctx, config, link, session) {
41
+ const logger = ctx.logger(`share-links-analysis:${exports.name}`);
42
+ const url = link.url;
43
+ let page = null;
44
+ try {
45
+ page = await ctx.puppeteer.page();
46
+ await page.setUserAgent(config.userAgent);
47
+ // 设置请求拦截(阻止非必要资源加载加速解析)
48
+ await page.setRequestInterception(true);
49
+ page.on('request', (req) => {
50
+ const resourceType = req.resourceType();
51
+ // 必须加载的资源
52
+ if (url.includes('/app/community/detail/') ||
53
+ url.includes('heybox-bbs') ||
54
+ url.includes('heybox-common') ||
55
+ resourceType === 'xhr' ||
56
+ resourceType === 'script') {
57
+ req.continue();
58
+ return;
59
+ }
60
+ // 阻止非必要资源
61
+ if (['image', 'stylesheet', 'font', 'media'].includes(resourceType) &&
62
+ !url.includes('avatar') &&
63
+ !url.includes('thumb')) {
64
+ req.abort();
65
+ }
66
+ else {
67
+ req.continue();
68
+ }
69
+ });
70
+ // 导航到目标页面
71
+ const response = await Promise.race([
72
+ page.goto(link.url, { waitUntil: 'networkidle2', timeout: 10000 }),
73
+ new Promise((_, reject) => setTimeout(() => reject(new Error('页面加载超时')), 10000))
74
+ ]);
75
+ if (!response || response.status() !== 200) {
76
+ throw new Error(`页面加载失败,状态码: ${response?.status() || '未知'}`);
77
+ }
78
+ // 智能等待 - 适配两种布局
79
+ await Promise.race([
80
+ page.waitForFunction(() => {
81
+ return document.querySelector('.hb-bbs-image-text') ||
82
+ document.querySelector('.hb-bbs-post');
83
+ }, { timeout: 30000 }),
84
+ new Promise((_, reject) => setTimeout(() => reject(new Error('核心内容容器未找到')), 30000))
85
+ ]);
86
+ // 全面解析页面内容
87
+ const postData = await page.evaluate(() => {
88
+ // 1. 检测页面类型
89
+ const isImageTextType = !!document.querySelector('.hb-bbs-image-text');
90
+ const isPostType = !!document.querySelector('.hb-bbs-post');
91
+ if (!isImageTextType && !isPostType) {
92
+ throw new Error('不支持的页面结构');
93
+ }
94
+ // 2. 基础数据解析(通用)
95
+ let title = '';
96
+ let username = '未知用户';
97
+ let level = 'Lv.0';
98
+ let time = '未知时间';
99
+ let ip = '未知地区';
100
+ const tags = [];
101
+ const contentBlocks = [];
102
+ let authorSection = null;
103
+ let coverImage = '';
104
+ // 3. 操作数据解析(通用)
105
+ let likeCount = '0';
106
+ let favoriteCount = '0';
107
+ let commentCount = '0';
108
+ const operationBox = document.querySelector('.link-reply__operation-box');
109
+ if (operationBox) {
110
+ const buttons = operationBox.querySelectorAll('button');
111
+ buttons.forEach((button) => {
112
+ const icon = button.querySelector('i');
113
+ const countSpan = button.querySelector('.link-reply__operation-desc');
114
+ const count = countSpan?.textContent?.trim() || '0';
115
+ if (icon?.classList.contains('heybox-bbs_thumbs-up')) {
116
+ likeCount = count;
117
+ }
118
+ else if (icon?.classList.contains('heybox-common_star')) {
119
+ favoriteCount = count;
120
+ }
121
+ else if (icon?.classList.contains('heybox-bbs_comment')) {
122
+ commentCount = count;
123
+ }
124
+ });
125
+ }
126
+ // 4. 按类型解析内容
127
+ if (isImageTextType) {
128
+ // ===== 旧结构解析 (.hb-bbs-image-text) =====
129
+ const container = document.querySelector('.hb-bbs-image-text');
130
+ // 标题
131
+ title = container.querySelector('.section-title__content')?.textContent?.trim() || '无标题';
132
+ // 作者信息
133
+ authorSection = container.querySelector('.link-section-user');
134
+ if (authorSection) {
135
+ username = authorSection.querySelector('.link-user__username')?.textContent?.trim() || '未知用户';
136
+ level = authorSection.querySelector('.level-tag__wrapper')?.textContent?.trim() || 'Lv.0';
137
+ time = authorSection.querySelector('.link-data__time')?.textContent?.trim() || '未知时间';
138
+ ip = authorSection.querySelector('.link-data__ip')?.textContent?.trim() || '未知地区';
139
+ }
140
+ // 标签
141
+ container.querySelectorAll('.link-section-tags .content-tag-text').forEach(tag => {
142
+ const text = tag.textContent?.trim();
143
+ if (text)
144
+ tags.push(text);
145
+ });
146
+ // 内容
147
+ const mainContent = container.querySelector('.image-text__content')?.textContent?.trim() || '';
148
+ contentBlocks.push({ type: 'text', content: mainContent });
149
+ // 图片(轮播图)
150
+ container.querySelectorAll('.header-image__item-image img').forEach((img, index) => {
151
+ const src = img.src.replace(/\?.*$/, '');
152
+ contentBlocks.push({ type: 'image', content: src });
153
+ });
154
+ }
155
+ else if (isPostType) {
156
+ const container = document.querySelector('.hb-bbs-post');
157
+ const postContainer = container.querySelector('.post__container');
158
+ // 标题
159
+ title = postContainer.querySelector('.section-title__content')?.textContent?.trim() || '无标题';
160
+ // 作者信息
161
+ authorSection = postContainer.querySelector('.link-section-user');
162
+ if (authorSection) {
163
+ username = authorSection.querySelector('.link-user__username')?.textContent?.trim() || '未知用户';
164
+ level = authorSection.querySelector('.level-tag__wrapper')?.textContent?.trim() || 'Lv.0';
165
+ // 时间/IP 在子元素中
166
+ const metaData = authorSection.querySelector('.user-info__line-2');
167
+ if (metaData) {
168
+ time = metaData.querySelector('.link-data__time')?.textContent?.trim() || '未知时间';
169
+ ip = metaData.querySelector('.link-data__ip')?.textContent?.replace('·', '').trim() || '未知地区';
170
+ }
171
+ }
172
+ // 标签(使用第一个标签区域)
173
+ postContainer.querySelectorAll('.link-section-tags:first-of-type .content-tag-text').forEach(tag => {
174
+ const text = tag.textContent?.trim();
175
+ if (text)
176
+ tags.push(text);
177
+ });
178
+ // 封面图片
179
+ const headerImageContainer = container.querySelector('.post__header-image');
180
+ if (headerImageContainer) {
181
+ const headerImage = headerImageContainer.querySelector('img');
182
+ if (headerImage) {
183
+ coverImage = headerImage.src.replace(/\?.*$/, '');
184
+ }
185
+ }
186
+ // 正文内容
187
+ const contentContainer = postContainer.querySelector('.post__content');
188
+ if (contentContainer) {
189
+ // 遍历所有子节点保持顺序
190
+ Array.from(contentContainer.childNodes).forEach(node => {
191
+ if (node.nodeType !== Node.ELEMENT_NODE)
192
+ return;
193
+ const el = node;
194
+ // 文本段落
195
+ if (el.matches('p.com-text, p.com-origin-source')) {
196
+ let text = el.textContent?.trim() || '';
197
+ // 特殊处理来源声明
198
+ if (el.classList.contains('com-origin-source')) {
199
+ text = `🔖 ${text}`;
200
+ }
201
+ if (text)
202
+ contentBlocks.push({ type: 'text', content: text });
203
+ }
204
+ // 图片
205
+ else if (el.matches('div.com-img, div.hb-cpt__image')) {
206
+ // 优先查找带'show'类的图片
207
+ const img = el.querySelector('img.hb-cpt__image-elem.show') ||
208
+ el.querySelector('img.hb-cpt__image-elem');
209
+ if (img) {
210
+ const src = img.src.replace(/\?.*$/, '');
211
+ contentBlocks.push({ type: 'image', content: src });
212
+ }
213
+ }
214
+ });
215
+ }
216
+ }
217
+ return {
218
+ // @ts-ignore
219
+ isImageTextType,
220
+ isPostType,
221
+ title,
222
+ username,
223
+ level,
224
+ time,
225
+ ip,
226
+ tags,
227
+ contentBlocks,
228
+ likeCount,
229
+ favoriteCount,
230
+ commentCount,
231
+ coverImage
232
+ };
233
+ });
234
+ if (!postData)
235
+ throw new Error('未找到有效内容');
236
+ // 确保页面关闭
237
+ if (page)
238
+ await page.close().catch(() => {
239
+ });
240
+ // 5. 构建消息
241
+ let mainbody = "";
242
+ // 内容块
243
+ postData.contentBlocks.forEach((block) => {
244
+ if (block.type === 'text') {
245
+ mainbody += (0, utils_1.escapeHtml)(block.content + "\n");
246
+ }
247
+ else if (block.type === 'image') {
248
+ mainbody += koishi_1.h.image(block.content).toString();
249
+ }
250
+ });
251
+ const tag = postData.tags.length ? `标签:${postData.tags.join(' | ')}\n` : '';
252
+ const status = `点赞: ${(0, utils_1.numeral)(postData.likeCount, config)} | 收藏: ${(0, utils_1.numeral)(postData.favoriteCount, config)} | 评论: ${(0, utils_1.numeral)(postData.commentCount, config)}`;
253
+ return {
254
+ platform: exports.name,
255
+ title: postData.title,
256
+ authorName: postData.username,
257
+ mainbody: mainbody + tag,
258
+ sourceUrl: link.url,
259
+ stats: postData.isImageTextType ? status : "",
260
+ coverUrl: postData.coverImage,
261
+ files: []
262
+ };
263
+ }
264
+ catch (error) {
265
+ logger.error('解析失败:', error);
266
+ return null;
267
+ }
268
+ }
@@ -1,5 +1,6 @@
1
1
  import { Context, Session } from 'koishi';
2
2
  import { Link, ParsedInfo, PluginConfig } from '../types';
3
+ export declare const name = "xiaohongshu";
3
4
  /**
4
5
  * 在文本中匹配小红书链接 (长链接或短链接)
5
6
  * @param content 消息内容
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  // src/parsers/xiaohongshu.ts
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.name = void 0;
4
5
  exports.match = match;
5
6
  exports.init = init;
6
7
  exports.process = process;
7
8
  const koishi_1 = require("koishi");
8
9
  const cheerio_1 = require("cheerio");
9
10
  const utils_1 = require("../utils");
11
+ exports.name = "xiaohongshu";
10
12
  const linkRules = [
11
13
  {
12
14
  pattern: /(?:https?:\/\/)?(?:www\.xiaohongshu\.com\/discovery\/item\/)([\w?=&\-.%]+)/gi,
@@ -45,7 +47,7 @@ function match(content) {
45
47
  continue;
46
48
  seen.add(url);
47
49
  results.push({
48
- platform: 'xiaohongshu',
50
+ platform: exports.name,
49
51
  type,
50
52
  id: cleanId,
51
53
  url,
@@ -61,7 +63,6 @@ function match(content) {
61
63
  */
62
64
  async function init(ctx, config) {
63
65
  const logger = ctx.logger('share-links-analysis:xiaohongshu');
64
- const platformId = 'xiaohongshu';
65
66
  if (!ctx.puppeteer) {
66
67
  logger.warn('Puppeteer 服务未启用,无法自动刷新 Cookie。');
67
68
  return false;
@@ -136,8 +137,7 @@ async function init(ctx, config) {
136
137
  * @returns 处理后的标准格式对象
137
138
  */
138
139
  async function process(ctx, config, link, session) {
139
- const logger = ctx.logger('share-links-analysis:xiaohongshu');
140
- const platformId = 'xiaohongshu';
140
+ const logger = ctx.logger(`share-links-analysis:${exports.name}`);
141
141
  // 步骤一:从原始分享链接中提取 xsec_token
142
142
  let token = null;
143
143
  try {
@@ -271,7 +271,7 @@ async function process(ctx, config, link, session) {
271
271
  files = [{ type: "video", url: videoUrl }];
272
272
  }
273
273
  return {
274
- platform: 'xiaohongshu',
274
+ platform: exports.name,
275
275
  title: noteData.title,
276
276
  authorName: noteData.user.nickname,
277
277
  mainbody: mainbody,
package/lib/types.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface PluginConfig {
24
24
  Min_Interval: number;
25
25
  waitTip_Switch: false | string;
26
26
  useForward: 'plain' | 'forward' | 'mixed';
27
+ usingLocal: boolean;
27
28
  sendFiles: boolean;
28
29
  sendLinks: boolean;
29
30
  format: string;
@@ -122,3 +123,22 @@ export interface XhsInitialState {
122
123
  };
123
124
  };
124
125
  }
126
+ export interface ContentBlock {
127
+ type: 'text' | 'image';
128
+ content: string;
129
+ }
130
+ export interface XiaoHeiHePostData {
131
+ isImageTextType: boolean;
132
+ isPostType: boolean;
133
+ title: string;
134
+ username: string;
135
+ level: string;
136
+ time: string;
137
+ ip: string;
138
+ tags: string[];
139
+ contentBlocks: ContentBlock[];
140
+ likeCount: string;
141
+ favoriteCount: string;
142
+ commentCount: string;
143
+ coverImage: string;
144
+ }
package/lib/utils.d.ts CHANGED
@@ -8,6 +8,7 @@ import { Context, 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 unescapeHtml(str: string): string;
11
12
  export declare function getFileSize(url: string, proxy: string | undefined, userAgent: string | undefined, logger: Logger): Promise<number | null>;
12
13
  export declare function getEffectiveSettings(ctx: Context, guildId: string | undefined, config: PluginConfig): Promise<{
13
14
  parsers: any;
package/lib/utils.js CHANGED
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.numeral = numeral;
40
40
  exports.escapeHtml = escapeHtml;
41
+ exports.unescapeHtml = unescapeHtml;
41
42
  exports.getFileSize = getFileSize;
42
43
  exports.getEffectiveSettings = getEffectiveSettings;
43
44
  exports.isUserAdmin = isUserAdmin;
@@ -78,6 +79,15 @@ function escapeHtml(str) {
78
79
  .replace(/"/g, '&quot;')
79
80
  .replace(/'/g, '&#39;');
80
81
  }
82
+ function unescapeHtml(str) {
83
+ if (!str)
84
+ return '';
85
+ return str.replace(/&quot;/g, '"')
86
+ .replace(/&#39;/g, "'")
87
+ .replace(/&lt;/g, '<')
88
+ .replace(/&gt;/g, '>')
89
+ .replace(/&amp;/g, '&');
90
+ }
81
91
  function getProxyAgent(proxy, url) {
82
92
  if (!proxy)
83
93
  return undefined;
@@ -327,14 +337,19 @@ async function sendResult_plain(session, config, result, logger) {
327
337
  }
328
338
  // --- 下载封面 ---
329
339
  if (result.coverUrl) {
330
- try {
331
- mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
332
- if (config.logLevel === 'full')
333
- logger.info(`封面已下载: ${mediaCoverUrl}`);
340
+ if (config.usingLocal) {
341
+ try {
342
+ mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
343
+ if (config.logLevel === 'full')
344
+ logger.info(`封面已下载: ${mediaCoverUrl}`);
345
+ }
346
+ catch (e) {
347
+ logger.warn(`封面下载失败: ${result.coverUrl}`, e);
348
+ mediaCoverUrl = result.coverUrl;
349
+ }
334
350
  }
335
- catch (e) {
336
- logger.warn(`封面下载失败: ${result.coverUrl}`, e);
337
- mediaCoverUrl = '';
351
+ else {
352
+ mediaCoverUrl = result.coverUrl;
338
353
  }
339
354
  }
340
355
  // --- 下载 mainbody 中的图片 ---
@@ -343,14 +358,19 @@ async function sendResult_plain(session, config, result, logger) {
343
358
  const urlMap = {};
344
359
  await Promise.all(imgMatches.map(async (match) => {
345
360
  const remoteUrl = match[1];
346
- try {
347
- const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
348
- urlMap[remoteUrl] = localUrl;
349
- if (config.logLevel === 'full')
350
- logger.info(`正文图片已下载: ${localUrl}`);
361
+ if (config.usingLocal) {
362
+ try {
363
+ const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
364
+ urlMap[remoteUrl] = localUrl;
365
+ if (config.logLevel === 'full')
366
+ logger.info(`正文图片已下载: ${localUrl}`);
367
+ }
368
+ catch (e) {
369
+ logger.warn(`正文图片下载失败: ${remoteUrl}`, e);
370
+ }
351
371
  }
352
- catch (e) {
353
- logger.warn(`正文图片下载失败: ${remoteUrl}`, e);
372
+ else {
373
+ urlMap[remoteUrl] = remoteUrl;
354
374
  }
355
375
  }));
356
376
  mediaMainbody = result.mainbody;
@@ -399,7 +419,9 @@ async function sendResult_plain(session, config, result, logger) {
399
419
  }
400
420
  if (shouldSend) {
401
421
  try {
402
- const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
422
+ let localUrl = remoteUrl;
423
+ if (config.usingLocal)
424
+ localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
403
425
  if (!localUrl)
404
426
  continue;
405
427
  let element = null;
@@ -441,7 +463,7 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
441
463
  const localDownloadDir = config.localDownloadDir;
442
464
  const onebotReadDir = config.onebotReadDir;
443
465
  let mediaCoverUrl = result.coverUrl;
444
- let mediaMainbody = result.mainbody;
466
+ let mediaMainbody = unescapeHtml(result.mainbody ?? '');
445
467
  let proxy = undefined;
446
468
  if (config.proxy_settings[result.platform]) {
447
469
  proxy = config.proxy;
@@ -449,12 +471,17 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
449
471
  }
450
472
  // --- 封面 ---
451
473
  if (result.coverUrl) {
452
- try {
453
- mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
474
+ if (config.usingLocal) {
475
+ try {
476
+ mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
477
+ }
478
+ catch (e) {
479
+ logger.warn('封面下载失败', e);
480
+ mediaCoverUrl = '';
481
+ }
454
482
  }
455
- catch (e) {
456
- logger.warn('封面下载失败', e);
457
- mediaCoverUrl = '';
483
+ else {
484
+ mediaCoverUrl = result.coverUrl;
458
485
  }
459
486
  }
460
487
  // --- mainbody 图片 ---
@@ -462,11 +489,16 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
462
489
  const imgUrls = [...result.mainbody.matchAll(/<img\s[^>]*src\s*=\s*["']?([^"'>\s]+)["']?/gi)].map(m => m[1]);
463
490
  const urlMap = {};
464
491
  await Promise.all(imgUrls.map(async (url) => {
465
- try {
466
- urlMap[url] = await downloadAndMapUrl(url, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
492
+ if (config.usingLocal) {
493
+ try {
494
+ urlMap[url] = await downloadAndMapUrl(url, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
495
+ }
496
+ catch (e) {
497
+ logger.warn(`正文图片下载失败: ${url}`, e);
498
+ }
467
499
  }
468
- catch (e) {
469
- logger.warn(`正文图片下载失败: ${url}`, e);
500
+ else {
501
+ urlMap[url] = url;
470
502
  }
471
503
  }));
472
504
  mediaMainbody = result.mainbody;
@@ -557,7 +589,9 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
557
589
  }
558
590
  if (shouldInclude) {
559
591
  try {
560
- const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
592
+ let localUrl = remoteUrl;
593
+ if (config.usingLocal)
594
+ localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
561
595
  if (!localUrl)
562
596
  continue;
563
597
  if (!mixed_sending) {
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.6.3",
5
+ "version": "0.7.1",
6
6
  "main": "lib/index.js",
7
7
  "typings": "lib/index.d.ts",
8
8
  "files": [