koishi-plugin-share-links-analysis 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/core.d.ts +1 -1
- package/lib/core.js +3 -2
- package/lib/index.js +3 -4
- package/lib/parsers/bilibili.d.ts +1 -0
- package/lib/parsers/bilibili.js +6 -4
- package/lib/parsers/twitter.d.ts +1 -0
- package/lib/parsers/twitter.js +5 -7
- package/lib/parsers/xiaoheihe.d.ts +5 -0
- package/lib/parsers/xiaoheihe.js +267 -0
- package/lib/parsers/xiaohongshu.d.ts +1 -0
- package/lib/parsers/xiaohongshu.js +5 -5
- package/lib/types.d.ts +20 -0
- package/lib/utils.js +49 -25
- package/package.json +1 -1
package/lib/core.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Session } from 'koishi';
|
|
2
2
|
import { Link, PluginConfig, ParsedInfo } from './types';
|
|
3
|
-
export declare const parsers_str:
|
|
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 =
|
|
46
|
+
const parsers = [Bilibili, Xiaohongshu, Twitter, Xiaoheihe];
|
|
47
|
+
exports.parsers_str = parsers.map(p => p.name);
|
|
47
48
|
/**
|
|
48
49
|
* 从文本中解析出所有支持的链接
|
|
49
50
|
* @param content 消息内容
|
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("基础设置"),
|
|
@@ -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
|
-
|
|
112
|
+
const isValidParser = (name) => core_1.parsers_str.includes(name);
|
|
113
|
+
if (!isValidParser(parser))
|
|
112
114
|
return '请输入正确的解析器名称';
|
|
113
115
|
if (!value)
|
|
114
116
|
return '请输入正确的模式';
|
|
@@ -192,9 +194,6 @@ function apply(ctx, config) {
|
|
|
192
194
|
lastProcessedUrls[channelId][link.url] = now;
|
|
193
195
|
await sendResult(session, config, result, logger);
|
|
194
196
|
}
|
|
195
|
-
else if (config.showError) {
|
|
196
|
-
await session.send(`无法解析链接:${link.url}。可能是不支持的类型或链接有误。`);
|
|
197
|
-
}
|
|
198
197
|
linkCount++;
|
|
199
198
|
}
|
|
200
199
|
});
|
package/lib/parsers/bilibili.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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(
|
|
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:
|
|
185
|
+
platform: exports.name,
|
|
184
186
|
title: data.title,
|
|
185
187
|
authorName: data.owner.name,
|
|
186
188
|
mainbody: (0, utils_1.escapeHtml)(data.desc),
|
package/lib/parsers/twitter.d.ts
CHANGED
package/lib/parsers/twitter.js
CHANGED
|
@@ -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:
|
|
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(
|
|
57
|
+
const logger = ctx.logger(`share-links-analysis:${exports.name}`);
|
|
59
58
|
let apiUrl;
|
|
60
59
|
if (link.type === 'short') {
|
|
61
60
|
// 短链接需先解析,但 vxtwitter 支持直接转换
|
|
@@ -113,7 +112,7 @@ async function process(ctx, config, link, session) {
|
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
return {
|
|
116
|
-
platform:
|
|
115
|
+
platform: exports.name,
|
|
117
116
|
title: `@${tweetData.user_screen_name} 的推文`,
|
|
118
117
|
authorName: tweetData.user_name || tweetData.user_screen_name,
|
|
119
118
|
mainbody: mainbody,
|
|
@@ -161,7 +160,6 @@ function parseMedia(tweetData) {
|
|
|
161
160
|
preview_url: media.thumbnail_url,
|
|
162
161
|
duration: media.duration_millis ? media.duration_millis / 1000 : undefined
|
|
163
162
|
});
|
|
164
|
-
continue;
|
|
165
163
|
}
|
|
166
164
|
}
|
|
167
165
|
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,267 @@
|
|
|
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: 10000 }),
|
|
84
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('核心内容容器未找到')), 10000))
|
|
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
|
+
isImageTextType,
|
|
219
|
+
isPostType,
|
|
220
|
+
title,
|
|
221
|
+
username,
|
|
222
|
+
level,
|
|
223
|
+
time,
|
|
224
|
+
ip,
|
|
225
|
+
tags,
|
|
226
|
+
contentBlocks,
|
|
227
|
+
likeCount,
|
|
228
|
+
favoriteCount,
|
|
229
|
+
commentCount,
|
|
230
|
+
coverImage
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
if (!postData)
|
|
234
|
+
throw new Error('未找到有效内容');
|
|
235
|
+
// 确保页面关闭
|
|
236
|
+
if (page)
|
|
237
|
+
await page.close().catch(() => {
|
|
238
|
+
});
|
|
239
|
+
// 5. 构建消息
|
|
240
|
+
let mainbody = "";
|
|
241
|
+
// 内容块
|
|
242
|
+
postData.contentBlocks.forEach((block) => {
|
|
243
|
+
if (block.type === 'text') {
|
|
244
|
+
mainbody += (0, utils_1.escapeHtml)(block.content + "\n");
|
|
245
|
+
}
|
|
246
|
+
else if (block.type === 'image') {
|
|
247
|
+
mainbody += koishi_1.h.image(block.content).toString();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
const tag = postData.tags.length ? `标签:${postData.tags.join(' | ')}\n` : '';
|
|
251
|
+
const status = `点赞: ${(0, utils_1.numeral)(postData.likeCount, config)} | 收藏: ${(0, utils_1.numeral)(postData.favoriteCount, config)} | 评论: ${(0, utils_1.numeral)(postData.commentCount, config)}`;
|
|
252
|
+
return {
|
|
253
|
+
platform: exports.name,
|
|
254
|
+
title: postData.title,
|
|
255
|
+
authorName: postData.username,
|
|
256
|
+
mainbody: mainbody + tag,
|
|
257
|
+
sourceUrl: link.url,
|
|
258
|
+
stats: postData.isImageTextType ? status : "",
|
|
259
|
+
coverUrl: postData.coverImage,
|
|
260
|
+
files: []
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
logger.error('解析失败:', error);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -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:
|
|
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(
|
|
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:
|
|
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.js
CHANGED
|
@@ -327,14 +327,19 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
327
327
|
}
|
|
328
328
|
// --- 下载封面 ---
|
|
329
329
|
if (result.coverUrl) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
330
|
+
if (config.usingLocal) {
|
|
331
|
+
try {
|
|
332
|
+
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
333
|
+
if (config.logLevel === 'full')
|
|
334
|
+
logger.info(`封面已下载: ${mediaCoverUrl}`);
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
logger.warn(`封面下载失败: ${result.coverUrl}`, e);
|
|
338
|
+
mediaCoverUrl = result.coverUrl;
|
|
339
|
+
}
|
|
334
340
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
mediaCoverUrl = '';
|
|
341
|
+
else {
|
|
342
|
+
mediaCoverUrl = result.coverUrl;
|
|
338
343
|
}
|
|
339
344
|
}
|
|
340
345
|
// --- 下载 mainbody 中的图片 ---
|
|
@@ -343,14 +348,19 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
343
348
|
const urlMap = {};
|
|
344
349
|
await Promise.all(imgMatches.map(async (match) => {
|
|
345
350
|
const remoteUrl = match[1];
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
+
if (config.usingLocal) {
|
|
352
|
+
try {
|
|
353
|
+
const localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
354
|
+
urlMap[remoteUrl] = localUrl;
|
|
355
|
+
if (config.logLevel === 'full')
|
|
356
|
+
logger.info(`正文图片已下载: ${localUrl}`);
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
logger.warn(`正文图片下载失败: ${remoteUrl}`, e);
|
|
360
|
+
}
|
|
351
361
|
}
|
|
352
|
-
|
|
353
|
-
|
|
362
|
+
else {
|
|
363
|
+
urlMap[remoteUrl] = remoteUrl;
|
|
354
364
|
}
|
|
355
365
|
}));
|
|
356
366
|
mediaMainbody = result.mainbody;
|
|
@@ -399,7 +409,9 @@ async function sendResult_plain(session, config, result, logger) {
|
|
|
399
409
|
}
|
|
400
410
|
if (shouldSend) {
|
|
401
411
|
try {
|
|
402
|
-
|
|
412
|
+
let localUrl = remoteUrl;
|
|
413
|
+
if (config.usingLocal)
|
|
414
|
+
localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
403
415
|
if (!localUrl)
|
|
404
416
|
continue;
|
|
405
417
|
let element = null;
|
|
@@ -449,12 +461,17 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
449
461
|
}
|
|
450
462
|
// --- 封面 ---
|
|
451
463
|
if (result.coverUrl) {
|
|
452
|
-
|
|
453
|
-
|
|
464
|
+
if (config.usingLocal) {
|
|
465
|
+
try {
|
|
466
|
+
mediaCoverUrl = await downloadAndMapUrl(result.coverUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
logger.warn('封面下载失败', e);
|
|
470
|
+
mediaCoverUrl = '';
|
|
471
|
+
}
|
|
454
472
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
mediaCoverUrl = '';
|
|
473
|
+
else {
|
|
474
|
+
mediaCoverUrl = result.coverUrl;
|
|
458
475
|
}
|
|
459
476
|
}
|
|
460
477
|
// --- mainbody 图片 ---
|
|
@@ -462,11 +479,16 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
462
479
|
const imgUrls = [...result.mainbody.matchAll(/<img\s[^>]*src\s*=\s*["']?([^"'>\s]+)["']?/gi)].map(m => m[1]);
|
|
463
480
|
const urlMap = {};
|
|
464
481
|
await Promise.all(imgUrls.map(async (url) => {
|
|
465
|
-
|
|
466
|
-
|
|
482
|
+
if (config.usingLocal) {
|
|
483
|
+
try {
|
|
484
|
+
urlMap[url] = await downloadAndMapUrl(url, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
logger.warn(`正文图片下载失败: ${url}`, e);
|
|
488
|
+
}
|
|
467
489
|
}
|
|
468
|
-
|
|
469
|
-
|
|
490
|
+
else {
|
|
491
|
+
urlMap[url] = url;
|
|
470
492
|
}
|
|
471
493
|
}));
|
|
472
494
|
mediaMainbody = result.mainbody;
|
|
@@ -557,7 +579,9 @@ async function sendResult_forward(session, config, result, logger, mixed_sending
|
|
|
557
579
|
}
|
|
558
580
|
if (shouldInclude) {
|
|
559
581
|
try {
|
|
560
|
-
|
|
582
|
+
let localUrl = remoteUrl;
|
|
583
|
+
if (config.usingLocal)
|
|
584
|
+
localUrl = await downloadAndMapUrl(remoteUrl, proxy, config.userAgent, localDownloadDir, onebotReadDir, logger);
|
|
561
585
|
if (!localUrl)
|
|
562
586
|
continue;
|
|
563
587
|
if (!mixed_sending) {
|