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 +4 -5
- package/lib/core.d.ts +1 -1
- package/lib/core.js +6 -3
- package/lib/index.js +16 -7
- 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 +7 -8
- package/lib/parsers/xiaoheihe.d.ts +5 -0
- package/lib/parsers/xiaoheihe.js +268 -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.d.ts +1 -0
- package/lib/utils.js +60 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
特别鸣谢以下项目的支持:
|
|
12
12
|
|
|
13
|
-
- [
|
|
14
|
-
- [koishi-plugin-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
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 消息内容
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
});
|
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 支持直接转换
|
|
@@ -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
|
-
|
|
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:
|
|
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,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.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, '"')
|
|
79
80
|
.replace(/'/g, ''');
|
|
80
81
|
}
|
|
82
|
+
function unescapeHtml(str) {
|
|
83
|
+
if (!str)
|
|
84
|
+
return '';
|
|
85
|
+
return str.replace(/"/g, '"')
|
|
86
|
+
.replace(/'/g, "'")
|
|
87
|
+
.replace(/</g, '<')
|
|
88
|
+
.replace(/>/g, '>')
|
|
89
|
+
.replace(/&/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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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) {
|