koishi-plugin-share-links-analysis 0.1.0 → 0.1.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/lib/core.d.ts +2 -2
- package/lib/core.js +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +71 -86
- package/lib/parsers/bilibili.d.ts +3 -3
- package/lib/parsers/bilibili.js +95 -87
- package/lib/parsers/xiaohongshu.d.ts +2 -2
- package/lib/parsers/xiaohongshu.js +61 -53
- package/lib/types.d.ts +15 -19
- package/package.json +1 -1
package/lib/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
-
import { Link, PluginConfig,
|
|
2
|
+
import { Link, PluginConfig, ParsedInfo } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* 从文本中解析出所有支持的链接
|
|
5
5
|
* @param content 消息内容
|
|
@@ -13,4 +13,4 @@ export declare function resolveLinks(content: string): Link[];
|
|
|
13
13
|
* @param link 解析出的链接对象
|
|
14
14
|
* @returns 处理后的链接结果,如果失败则返回 null
|
|
15
15
|
*/
|
|
16
|
-
export declare function processLink(ctx: Context, config: PluginConfig, link: Link): Promise<
|
|
16
|
+
export declare function processLink(ctx: Context, config: PluginConfig, link: Link): Promise<ParsedInfo | null>;
|
package/lib/core.js
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ export declare const inject: {
|
|
|
5
5
|
required: string[];
|
|
6
6
|
optional: string[];
|
|
7
7
|
};
|
|
8
|
-
export declare const usage = "\n\u5F00\u542F\u63D2\u4EF6\u540E\uFF0C\u5373\u53EF\u81EA\u52A8\u89E3\u6790\u5206\u4EAB\u94FE\u63A5\n\u5411Bot\u53D1\u9001B\u7AD9\u3001\u5C0F\u7EA2\u4E66\u7B49\u652F\u6301\u5E73\u53F0\u7684\u5206\u4EAB\u94FE\u63A5\uFF0C\u4F1A\u8FD4\u56DE\u56FE\u6587\u4FE1\u606F\u4E0E\u89C6\u9891\u3002\n\
|
|
8
|
+
export declare const usage = "\n\u5F00\u542F\u63D2\u4EF6\u540E\uFF0C\u5373\u53EF\u81EA\u52A8\u89E3\u6790\u5206\u4EAB\u94FE\u63A5\u3002\n\u5411Bot\u53D1\u9001B\u7AD9\u3001\u5C0F\u7EA2\u4E66\u7B49\u652F\u6301\u5E73\u53F0\u7684\u5206\u4EAB\u94FE\u63A5\uFF0C\u4F1A\u8FD4\u56DE\u56FE\u6587\u4FE1\u606F\u4E0E\u89C6\u9891\u3002\n\u60A8\u53EF\u4EE5\u5728\u63D2\u4EF6\u914D\u7F6E\u4E2D\u4E3A\u4E0D\u540C\u5E73\u53F0\u5206\u522B\u8BBE\u7F6E\u8FD4\u56DE\u7684\u56FE\u6587\u6D88\u606F\u683C\u5F0F\u3002\n";
|
|
9
9
|
export declare const Config: Schema<PluginConfig>;
|
|
10
10
|
export declare function apply(ctx: Context, config: PluginConfig): void;
|
package/lib/index.js
CHANGED
|
@@ -1,91 +1,76 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// src/index.ts
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports.Config = exports.usage = exports.inject = exports.name = void 0;
|
|
4
5
|
exports.apply = apply;
|
|
5
6
|
const koishi_1 = require("koishi");
|
|
6
7
|
const core_1 = require("./core");
|
|
7
|
-
// 这是插件的元数据
|
|
8
8
|
exports.name = 'share-links-analysis';
|
|
9
9
|
exports.inject = {
|
|
10
10
|
required: ['BiliBiliVideo'],
|
|
11
|
-
optional: ['puppeteer'],
|
|
11
|
+
optional: ['puppeteer'],
|
|
12
12
|
};
|
|
13
13
|
exports.usage = `
|
|
14
|
-
|
|
14
|
+
开启插件后,即可自动解析分享链接。
|
|
15
15
|
向Bot发送B站、小红书等支持平台的分享链接,会返回图文信息与视频。
|
|
16
|
-
|
|
17
|
-
B站短链接(b23.tv)解析需要 puppeteer 服务支持,请确保已安装并启用该插件。
|
|
16
|
+
您可以在插件配置中为不同平台分别设置返回的图文消息格式。
|
|
18
17
|
`;
|
|
18
|
+
// 配置文件
|
|
19
19
|
exports.Config = koishi_1.Schema.intersect([
|
|
20
20
|
koishi_1.Schema.object({
|
|
21
|
-
linktextParsing: koishi_1.Schema.boolean().default(true).description("是否返回图文数据。`开启后,才发送视频数据的图文解析。`"),
|
|
22
|
-
VideoParsing_ToLink: koishi_1.Schema.union([
|
|
23
|
-
koishi_1.Schema.const('1').description('不返回视频/视频直链'),
|
|
24
|
-
koishi_1.Schema.const('2').description('仅返回视频'),
|
|
25
|
-
koishi_1.Schema.const('3').description('仅返回视频直链'),
|
|
26
|
-
koishi_1.Schema.const('4').description('返回视频和视频直链'),
|
|
27
|
-
koishi_1.Schema.const('5').description('返回视频,仅在日志记录视频直链'),
|
|
28
|
-
]).role('radio').default('2').description("是否返回` 视频/视频直链 `"),
|
|
29
21
|
Video_ClarityPriority: koishi_1.Schema.union([
|
|
30
|
-
koishi_1.Schema.const('1').description('
|
|
31
|
-
koishi_1.Schema.const('2').description('
|
|
22
|
+
koishi_1.Schema.const('1').description('低清晰度优先'),
|
|
23
|
+
koishi_1.Schema.const('2').description('高清晰度优先'),
|
|
32
24
|
]).role('radio').default('1').description("发送的视频清晰度优先策略"),
|
|
33
|
-
Maximumduration: koishi_1.Schema.number().default(25).description("
|
|
34
|
-
Maximumduration_tip: koishi_1.Schema.
|
|
35
|
-
|
|
36
|
-
koishi_1.Schema.string().description('返回文字提示(请在右侧填写文字内容)').default('视频太长啦!还是去B站看吧~'),
|
|
37
|
-
]).description("对过长视频的文字提示内容").default('视频太长啦!还是去B站看吧~'),
|
|
38
|
-
MinimumTimeInterval: koishi_1.Schema.number().default(180).description("若干`秒`内不再处理相同链接 `防止多bot互相触发导致的刷屏/性能浪费`").min(1),
|
|
25
|
+
Maximumduration: koishi_1.Schema.number().default(25).description("允许解析的视频最大时长(分钟)").min(1),
|
|
26
|
+
Maximumduration_tip: koishi_1.Schema.string().default('视频太长啦!还是去平台官网看吧~').description("对过长视频的文字提示内容"),
|
|
27
|
+
MinimumTimeInterval: koishi_1.Schema.number().default(180).description("若干秒内不再处理相同链接,防止刷屏").min(1),
|
|
39
28
|
waitTip_Switch: koishi_1.Schema.union([
|
|
40
29
|
koishi_1.Schema.const(false).description('不返回文字提示'),
|
|
41
|
-
koishi_1.Schema.string().description('
|
|
30
|
+
koishi_1.Schema.string().description('返回文字提示'),
|
|
42
31
|
]).description("是否返回等待提示。开启后,会发送`等待提示语`").default(false),
|
|
43
32
|
}).description("基础设置"),
|
|
44
33
|
koishi_1.Schema.object({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
format: koishi_1.Schema.string().role('textarea').default(`{title}
|
|
35
|
+
{cover}
|
|
36
|
+
作者:{authorName}
|
|
37
|
+
简介:{description}
|
|
38
|
+
{stats}
|
|
39
|
+
{images}
|
|
40
|
+
{video}`).description('统一主输出格式。<br/>可用占位符: `{title}`, `{cover}`, `{authorName}`, `{description}`, `{stats}`, `{sourceUrl}`, `{images}`, `{video}`, `{videoUrl}`'),
|
|
41
|
+
}).description("格式化模板"),
|
|
42
|
+
koishi_1.Schema.object({
|
|
43
|
+
bilibiliStatsFormat: koishi_1.Schema.string().role('textarea').default('播放: {播放} | 弹幕: {弹幕} | 点赞: {点赞} | 硬币: {硬币} | 收藏: {收藏}')
|
|
44
|
+
.description('Bilibili 链接的数据统计格式。<br/>可用占位符: `{播放}`, `{弹幕}`, `{点赞}`, `{硬币}`, `{收藏}`'),
|
|
45
|
+
xiaohongshuStatsFormat: koishi_1.Schema.string().role('textarea').default('点赞: {点赞} | 收藏: {收藏} | 评论: {评论}')
|
|
46
|
+
.description('小红书链接的数据统计格式。<br/>可用占位符: `{点赞}`, `{收藏}`, `{评论}`'),
|
|
47
|
+
}).description("数据格式化"),
|
|
48
|
+
koishi_1.Schema.object({
|
|
49
|
+
parseLimit: koishi_1.Schema.number().default(3).description("单对话多链接解析上限"),
|
|
50
|
+
useNumeral: koishi_1.Schema.boolean().default(true).description("使用格式化数字 (如 10000 -> 1万)"),
|
|
51
|
+
showError: koishi_1.Schema.boolean().default(false).description("当链接不正确时提醒发送者"),
|
|
49
52
|
bVideoIDPreference: koishi_1.Schema.union([
|
|
50
53
|
koishi_1.Schema.const("bv").description("BV 号"),
|
|
51
54
|
koishi_1.Schema.const("av").description("AV 号"),
|
|
52
|
-
]).default("bv").description("B站ID 偏好")
|
|
53
|
-
|
|
54
|
-
bVideoOwner: koishi_1.Schema.boolean().default(true).description("显示 UP 主"),
|
|
55
|
-
bVideoDesc: koishi_1.Schema.boolean().default(false).description("显示简介`有的简介真的很长`"),
|
|
56
|
-
bVideoStat: koishi_1.Schema.boolean().default(true).description("显示状态(*三连数据*)"),
|
|
57
|
-
bVideoExtraStat: koishi_1.Schema.boolean().default(true).description("显示额外状态(*弹幕&观看*)"),
|
|
58
|
-
bVideoShowLink: koishi_1.Schema.boolean().default(false).description("显示视频链接`开启可能会导致其他bot循环解析`"),
|
|
59
|
-
}).description("B站内容解析设置"),
|
|
60
|
-
koishi_1.Schema.object({
|
|
61
|
-
xhsCover: koishi_1.Schema.boolean().default(true).description("显示首图/封面"),
|
|
62
|
-
xhsAuthor: koishi_1.Schema.boolean().default(true).description("显示作者"),
|
|
63
|
-
xhsDesc: koishi_1.Schema.boolean().default(true).description("显示简介"),
|
|
64
|
-
xhsStat: koishi_1.Schema.boolean().default(true).description("显示状态(*点赞、收藏、评论*)"),
|
|
65
|
-
}).description("小红书内容解析设置"),
|
|
55
|
+
]).default("bv").description("B站ID 偏好"),
|
|
56
|
+
}).description("高级解析设置"),
|
|
66
57
|
koishi_1.Schema.object({
|
|
67
58
|
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"),
|
|
68
|
-
|
|
59
|
+
logLevel: koishi_1.Schema.union([
|
|
60
|
+
koishi_1.Schema.const('none').description('不记录'),
|
|
61
|
+
koishi_1.Schema.const('link_only').description('仅记录视频直链'),
|
|
62
|
+
koishi_1.Schema.const('full').description('记录完整调试信息'),
|
|
63
|
+
]).role('radio').default('none').description("选择后台日志记录等级"),
|
|
69
64
|
}).description("调试设置"),
|
|
70
65
|
]);
|
|
71
|
-
// 主插件逻辑
|
|
72
66
|
function apply(ctx, config) {
|
|
73
67
|
const logger = ctx.logger('share-links-analysis');
|
|
74
68
|
const lastProcessedUrls = {};
|
|
75
69
|
ctx.middleware(async (session, next) => {
|
|
76
|
-
if (!session.content || !session.channelId)
|
|
70
|
+
if (!session.content || !session.channelId)
|
|
77
71
|
return next();
|
|
78
|
-
|
|
79
|
-
let content = session.content;
|
|
72
|
+
const content = session.content.replace(/\\/g, '');
|
|
80
73
|
const channelId = session.channelId;
|
|
81
|
-
if (config.BVnumberParsing) {
|
|
82
|
-
const bvPattern = /(?:^|\s)(BV[1-9A-HJ-NP-Za-km-z]{10})(?:\s|$)/g;
|
|
83
|
-
const bvMatches = content.match(bvPattern);
|
|
84
|
-
if (bvMatches) {
|
|
85
|
-
const urls = bvMatches.map(bv => `https://www.bilibili.com/video/${bv.trim()}`);
|
|
86
|
-
content += '\n' + urls.join('\n');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
74
|
const links = (0, core_1.resolveLinks)(content);
|
|
90
75
|
if (links.length === 0)
|
|
91
76
|
return next();
|
|
@@ -96,11 +81,10 @@ function apply(ctx, config) {
|
|
|
96
81
|
break;
|
|
97
82
|
}
|
|
98
83
|
const now = Date.now();
|
|
99
|
-
if (!lastProcessedUrls[channelId])
|
|
84
|
+
if (!lastProcessedUrls[channelId])
|
|
100
85
|
lastProcessedUrls[channelId] = {};
|
|
101
|
-
}
|
|
102
86
|
if (now - (lastProcessedUrls[channelId][link.url] || 0) < config.MinimumTimeInterval * 1000) {
|
|
103
|
-
if (config.
|
|
87
|
+
if (config.logLevel === 'full')
|
|
104
88
|
logger.info(`链接 ${link.url} 在冷却时间内,跳过处理。`);
|
|
105
89
|
continue;
|
|
106
90
|
}
|
|
@@ -117,44 +101,45 @@ function apply(ctx, config) {
|
|
|
117
101
|
}
|
|
118
102
|
linkCount++;
|
|
119
103
|
}
|
|
120
|
-
return next();
|
|
121
104
|
});
|
|
122
105
|
}
|
|
123
106
|
async function sendResult(session, config, result, logger) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
107
|
+
let message = config.format;
|
|
108
|
+
message = message.replace(/{title}/g, result.title || '');
|
|
109
|
+
message = message.replace(/{authorName}/g, result.authorName || '');
|
|
110
|
+
message = message.replace(/{description}/g, result.description ? result.description : '');
|
|
111
|
+
message = message.replace(/{sourceUrl}/g, result.sourceUrl || '');
|
|
112
|
+
message = message.replace(/{cover}/g, result.coverUrl ? koishi_1.h.image(result.coverUrl).toString() : '');
|
|
113
|
+
const imagesText = result.images ? result.images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
|
|
114
|
+
message = message.replace(/{images}/g, imagesText);
|
|
115
|
+
message = message.replace(/{stats}/g, result.stats || '');
|
|
116
|
+
// 【修复】只要 videoUrl 存在就处理,仅当 duration 明确超长时才替换为提示
|
|
117
|
+
if (result.videoUrl) {
|
|
118
|
+
// 仅当 duration 是有效数字且超长时,才显示提示
|
|
119
|
+
if (typeof result.duration === 'number' && result.duration > config.Maximumduration * 60) {
|
|
120
|
+
const tip = config.Maximumduration_tip || '';
|
|
121
|
+
message = message.replace(/{video}/g, tip);
|
|
122
|
+
message = message.replace(/{videoUrl}/g, '');
|
|
128
123
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (config.
|
|
134
|
-
|
|
124
|
+
else {
|
|
125
|
+
// 正常发送视频和链接
|
|
126
|
+
message = message.replace(/{video}/g, koishi_1.h.video(result.videoUrl).toString());
|
|
127
|
+
message = message.replace(/{videoUrl}/g, result.videoUrl);
|
|
128
|
+
if (config.logLevel === 'link_only' || config.logLevel === 'full') {
|
|
129
|
+
logger.info(`视频直链 (${result.platform}): ${result.videoUrl}`);
|
|
135
130
|
}
|
|
136
|
-
return;
|
|
137
131
|
}
|
|
138
132
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
case '4':
|
|
148
|
-
await session.send(koishi_1.h.text(result.videoUrl));
|
|
149
|
-
await session.send(koishi_1.h.video(result.videoUrl));
|
|
150
|
-
break;
|
|
151
|
-
case '5':
|
|
152
|
-
logger.info(`视频直链: ${result.videoUrl}`);
|
|
153
|
-
await session.send(koishi_1.h.video(result.videoUrl));
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
133
|
+
else {
|
|
134
|
+
// 没有视频则移除占位符
|
|
135
|
+
message = message.replace(/{video}/g, '');
|
|
136
|
+
message = message.replace(/{videoUrl}/g, '');
|
|
137
|
+
}
|
|
138
|
+
const cleanMessage = message.split('\n').filter(line => line.trim() !== '' || line.includes('<')).join('\n');
|
|
139
|
+
if (cleanMessage) {
|
|
140
|
+
await session.send(koishi_1.h.quote(session.messageId) + cleanMessage);
|
|
156
141
|
}
|
|
157
|
-
if (config.
|
|
142
|
+
if (config.logLevel === 'full') {
|
|
158
143
|
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
159
144
|
}
|
|
160
145
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
-
import { Link,
|
|
2
|
+
import { Link, ParsedInfo, PluginConfig } from '../types';
|
|
3
3
|
/**
|
|
4
|
-
* 在文本中匹配B站链接
|
|
4
|
+
* 在文本中匹配B站链接 (长链/短链/纯BV号)
|
|
5
5
|
* @param content 消息内容
|
|
6
6
|
* @returns 匹配到的链接对象数组
|
|
7
7
|
*/
|
|
@@ -13,4 +13,4 @@ export declare function match(content: string): Link[];
|
|
|
13
13
|
* @param link 匹配到的链接对象
|
|
14
14
|
* @returns 处理后的标准格式对象
|
|
15
15
|
*/
|
|
16
|
-
export declare function process(ctx: Context, config: PluginConfig, link: Link): Promise<
|
|
16
|
+
export declare function process(ctx: Context, config: PluginConfig, link: Link): Promise<ParsedInfo | null>;
|
package/lib/parsers/bilibili.js
CHANGED
|
@@ -1,33 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// src/parsers/bilibili.ts
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports.match = match;
|
|
4
5
|
exports.process = process;
|
|
5
|
-
const koishi_1 = require("koishi");
|
|
6
6
|
const utils_1 = require("../utils");
|
|
7
7
|
/**
|
|
8
|
-
* 在文本中匹配B站链接
|
|
8
|
+
* 在文本中匹配B站链接 (长链/短链/纯BV号)
|
|
9
9
|
* @param content 消息内容
|
|
10
10
|
* @returns 匹配到的链接对象数组
|
|
11
11
|
*/
|
|
12
12
|
function match(content) {
|
|
13
|
+
const results = [];
|
|
14
|
+
// 匹配标准长链接和短链接
|
|
13
15
|
const linkRegex = [
|
|
14
|
-
{ pattern: /(https?:\/\/)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z]+)/i, type: "video" },
|
|
15
|
-
{ pattern: /(https?:\/\/)?b23\.tv\/
|
|
16
|
+
{ pattern: /(https?:\/\/)?(www\.bilibili\.com\/video\/([ab]v[0-9a-zA-Z]+))/i, type: "video" },
|
|
17
|
+
{ pattern: /(https?:\/\/)?(b23\.tv\/[0-9a-zA-Z]+)/i, type: "short" },
|
|
16
18
|
];
|
|
17
|
-
const results = [];
|
|
18
19
|
for (const rule of linkRegex) {
|
|
19
20
|
const matches = [...content.matchAll(new RegExp(rule.pattern, 'gi'))];
|
|
20
21
|
for (const matchArr of matches) {
|
|
21
22
|
if (matchArr[2]) {
|
|
23
|
+
// 强制将所有链接格式化为 https 开头
|
|
24
|
+
const formattedUrl = `https://${matchArr[2]}`;
|
|
22
25
|
results.push({
|
|
23
26
|
platform: 'bilibili',
|
|
24
27
|
type: rule.type,
|
|
25
|
-
id: matchArr[
|
|
26
|
-
url:
|
|
28
|
+
id: rule.type === 'video' ? matchArr[3] : matchArr[2],
|
|
29
|
+
url: formattedUrl
|
|
27
30
|
});
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
}
|
|
34
|
+
// 匹配独立的 BV 号
|
|
35
|
+
const bvPattern = /(?<![a-zA-Z0-9/])(BV[1-9A-HJ-NP-Za-km-z]{10})(?![a-zA-Z0-9])/gi;
|
|
36
|
+
const bvMatches = [...content.matchAll(bvPattern)];
|
|
37
|
+
for (const matchArr of bvMatches) {
|
|
38
|
+
const videoId = matchArr[1];
|
|
39
|
+
results.push({
|
|
40
|
+
platform: 'bilibili',
|
|
41
|
+
type: 'video',
|
|
42
|
+
id: videoId,
|
|
43
|
+
url: `https://www.bilibili.com/video/${videoId}`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
31
46
|
return results;
|
|
32
47
|
}
|
|
33
48
|
/**
|
|
@@ -41,50 +56,46 @@ async function process(ctx, config, link) {
|
|
|
41
56
|
const logger = ctx.logger('share-links-analysis:bilibili');
|
|
42
57
|
let videoId = link.id;
|
|
43
58
|
let videoIdType = link.type;
|
|
44
|
-
// 如果是短链接,需要解析出真实ID
|
|
45
59
|
if (link.type === 'short') {
|
|
46
60
|
let finalUrl = '';
|
|
47
|
-
|
|
48
|
-
logger.info(`B站短链接解析:尝试使用轻量级HTTP方案解析 ${link.url}`);
|
|
61
|
+
logger.info(`B站短链接解析:尝试解析 ${link.url}`);
|
|
49
62
|
try {
|
|
50
63
|
const response = await ctx.http(link.url, {
|
|
51
64
|
method: 'GET',
|
|
52
65
|
headers: { 'User-Agent': config.userAgent },
|
|
53
66
|
redirect: 'manual',
|
|
54
67
|
});
|
|
55
|
-
// 【修正】使用 .get('location') 的标准方法来获取响应头
|
|
56
68
|
const locationHeader = response.headers.get('location');
|
|
69
|
+
if (locationHeader)
|
|
70
|
+
finalUrl = locationHeader;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
const locationHeader = e.response?.headers?.location;
|
|
57
74
|
if (locationHeader) {
|
|
58
75
|
finalUrl = locationHeader;
|
|
59
|
-
logger.info(`轻量级方案成功 (方式A:获取响应头),短链接指向: ${finalUrl}`);
|
|
60
76
|
}
|
|
61
|
-
else if (
|
|
62
|
-
|
|
63
|
-
if (match && match[1]) {
|
|
64
|
-
finalUrl = match[1];
|
|
65
|
-
logger.info(`轻量级方案成功 (方式B:解析响应体),短链接指向: ${finalUrl}`);
|
|
66
|
-
}
|
|
77
|
+
else if (config.logLevel === 'full') {
|
|
78
|
+
logger.debug(`解析短链接时发生网络错误或未找到跳转地址: ${e.message}`);
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
if (finalUrl && (finalUrl.includes('b23.tv') || finalUrl.includes('bilibili.com/'))) {
|
|
82
|
+
const urlObj = new URL(finalUrl);
|
|
83
|
+
if (urlObj.hostname === 'b23.tv' || (urlObj.hostname === 'bilibili.com' && !urlObj.pathname.startsWith('/video/'))) {
|
|
84
|
+
logger.warn(`标准HTTP解析方案未能解析到最终视频链接,仍然是短链接或非视频页: ${finalUrl}。切换至后备方案。`);
|
|
85
|
+
finalUrl = '';
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
|
-
// 【方案二】如果轻量级方案失败,并且安装了Puppeteer,则使用它作为后备
|
|
75
88
|
if (!finalUrl && ctx.puppeteer) {
|
|
76
|
-
logger.
|
|
89
|
+
logger.info(`标准解析方案失败或结果无效,切换至Puppeteer后备方案: ${link.url}`);
|
|
77
90
|
let page = null;
|
|
78
91
|
try {
|
|
79
92
|
page = await ctx.puppeteer.page();
|
|
80
93
|
await page.setUserAgent(config.userAgent);
|
|
81
94
|
await page.goto(link.url, { waitUntil: 'networkidle0' });
|
|
82
|
-
finalUrl = page.url();
|
|
83
|
-
logger.info(`Puppeteer方案成功,短链接指向: ${finalUrl}`);
|
|
95
|
+
finalUrl = page.url();
|
|
84
96
|
}
|
|
85
97
|
catch (e) {
|
|
86
|
-
|
|
87
|
-
logger.error(`Puppeteer方案解析短链接 ${link.url} 失败: ${message}`);
|
|
98
|
+
logger.error(`Puppeteer方案解析短链接 ${link.url} 失败: ${e.message}`);
|
|
88
99
|
return null;
|
|
89
100
|
}
|
|
90
101
|
finally {
|
|
@@ -92,86 +103,83 @@ async function process(ctx, config, link) {
|
|
|
92
103
|
await page.close();
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
if (finalUrl) {
|
|
107
|
+
if (config.logLevel === 'full')
|
|
108
|
+
logger.info(`短链接解析成功,指向: ${finalUrl}`);
|
|
109
|
+
const matchedLinks = match(finalUrl);
|
|
110
|
+
if (matchedLinks.length > 0 && matchedLinks[0].type === 'video') {
|
|
111
|
+
videoId = matchedLinks[0].id;
|
|
112
|
+
videoIdType = 'video';
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
logger.warn(`在最终链接中未找到有效的视频ID: ${finalUrl}`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
104
118
|
}
|
|
105
119
|
else {
|
|
106
|
-
logger.
|
|
120
|
+
logger.error('短链接解析失败,且无后备方案。');
|
|
107
121
|
return null;
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
|
-
// 如果最终没能得到视频ID,则解析失败
|
|
111
124
|
if (videoIdType !== 'video' || !videoId) {
|
|
112
125
|
logger.warn(`无法从链接 ${link.url} 中解析出有效的B站视频ID。`);
|
|
113
126
|
return null;
|
|
114
127
|
}
|
|
115
|
-
|
|
128
|
+
if (config.logLevel === 'full')
|
|
129
|
+
logger.info(`获取视频信息,ID: ${videoId}`);
|
|
116
130
|
const idType = videoId.startsWith('BV') ? 'bvid' : 'aid';
|
|
117
131
|
const infoUrl = `https://api.bilibili.com/x/web-interface/view?${idType}=${videoId}`;
|
|
118
|
-
let info;
|
|
119
132
|
try {
|
|
120
|
-
info = await ctx.http.get(infoUrl, {
|
|
133
|
+
const info = await ctx.http.get(infoUrl, {
|
|
121
134
|
headers: { 'User-Agent': config.userAgent }
|
|
122
135
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
const data = info.data;
|
|
134
|
-
// --- 格式化图文消息 ---
|
|
135
|
-
let text = `${data.title}\n`;
|
|
136
|
-
if (config.bVideoImage)
|
|
137
|
-
text += koishi_1.h.image(data.pic) + '\n';
|
|
138
|
-
if (config.bVideoOwner)
|
|
139
|
-
text += `UP主: ${data.owner.name}\n`;
|
|
140
|
-
if (config.bVideoDesc && data.desc)
|
|
141
|
-
text += `简介:${data.desc}\n`;
|
|
142
|
-
if (config.bVideoStat) {
|
|
143
|
-
text += `点赞: ${(0, utils_1.numeral)(data.stat.like, config)} | 硬币: ${(0, utils_1.numeral)(data.stat.coin, config)} | 收藏: ${(0, utils_1.numeral)(data.stat.favorite, config)}\n`;
|
|
144
|
-
}
|
|
145
|
-
if (config.bVideoExtraStat) {
|
|
146
|
-
text += `播放: ${(0, utils_1.numeral)(data.stat.view, config)} | 弹幕: ${(0, utils_1.numeral)(data.stat.danmaku, config)}\n`;
|
|
147
|
-
}
|
|
148
|
-
const sourceUrl = `https://www.bilibili.com/video/${data.bvid}`;
|
|
149
|
-
if (config.bVideoShowLink) {
|
|
150
|
-
text += sourceUrl;
|
|
151
|
-
}
|
|
152
|
-
// --- 获取视频直链 ---
|
|
153
|
-
let videoUrl = null;
|
|
154
|
-
if (config.VideoParsing_ToLink !== '1') {
|
|
155
|
-
logger.info(`尝试获取视频流,bvid: ${data.bvid}`);
|
|
136
|
+
if (!info || !info.data) {
|
|
137
|
+
logger.warn(`B站API未返回有效数据,ID ${videoId}。响应: ${JSON.stringify(info)}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const data = info.data;
|
|
141
|
+
// --- 获取视频直链 ---
|
|
142
|
+
let videoUrl = null;
|
|
143
|
+
if (config.logLevel === 'full')
|
|
144
|
+
logger.info(`尝试获取视频流,bvid: ${data.bvid}`);
|
|
156
145
|
try {
|
|
157
146
|
const videoStream = await ctx.BiliBiliVideo.getBilibiliVideoStream(data.aid, data.bvid, data.pages[0].cid, config.Video_ClarityPriority === '1' ? 32 : 80, 'html5', 1);
|
|
158
147
|
if (videoStream?.data?.durl?.[0]?.url) {
|
|
159
148
|
videoUrl = videoStream.data.durl[0].url;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
else {
|
|
163
|
-
logger.warn(`未能获取视频流,bvid: ${data.bvid}。API返回的流数据无效。响应: ${JSON.stringify(videoStream)}`);
|
|
149
|
+
if (config.logLevel === 'full')
|
|
150
|
+
logger.info(`成功获取视频流,bvid: ${data.bvid}`);
|
|
164
151
|
}
|
|
165
152
|
}
|
|
166
153
|
catch (e) {
|
|
167
|
-
|
|
168
|
-
logger.error(`通过BiliBiliVideo服务获取视频流失败,bvid: ${data.bvid}: ${message}`);
|
|
154
|
+
logger.error(`通过BiliBiliVideo服务获取视频流失败,bvid: ${data.bvid}: ${e.message}`);
|
|
169
155
|
}
|
|
156
|
+
// 在解析器内部格式化 stats 字符串
|
|
157
|
+
const stats = {
|
|
158
|
+
'播放': (0, utils_1.numeral)(data.stat.view, config),
|
|
159
|
+
'弹幕': (0, utils_1.numeral)(data.stat.danmaku, config),
|
|
160
|
+
'点赞': (0, utils_1.numeral)(data.stat.like, config),
|
|
161
|
+
'硬币': (0, utils_1.numeral)(data.stat.coin, config),
|
|
162
|
+
'收藏': (0, utils_1.numeral)(data.stat.favorite, config),
|
|
163
|
+
};
|
|
164
|
+
let statsString = config.bilibiliStatsFormat;
|
|
165
|
+
// 【修复】使用类型安全的方式遍历对象,防止 TypeScript 报错
|
|
166
|
+
Object.keys(stats).forEach(key => {
|
|
167
|
+
statsString = statsString.replace(`{${key}}`, stats[key]);
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
platform: 'bilibili',
|
|
171
|
+
title: data.title,
|
|
172
|
+
authorName: data.owner.name,
|
|
173
|
+
description: data.desc,
|
|
174
|
+
coverUrl: data.pic,
|
|
175
|
+
videoUrl: videoUrl,
|
|
176
|
+
duration: data.duration,
|
|
177
|
+
sourceUrl: `https://www.bilibili.com/video/${data.bvid}`,
|
|
178
|
+
stats: statsString, // 【修改】传入格式化后的字符串
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
logger.error(`获取或处理B站视频信息失败,ID ${videoId}: ${error.message}`);
|
|
183
|
+
return null;
|
|
170
184
|
}
|
|
171
|
-
return {
|
|
172
|
-
text: text.trim(),
|
|
173
|
-
videoUrl: videoUrl,
|
|
174
|
-
duration: data.duration,
|
|
175
|
-
sourceUrl: sourceUrl,
|
|
176
|
-
};
|
|
177
185
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
-
import { Link,
|
|
2
|
+
import { Link, ParsedInfo, PluginConfig } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* 在文本中匹配小红书链接 (长链接或短链接)
|
|
5
5
|
* @param content 消息内容
|
|
@@ -13,4 +13,4 @@ export declare function match(content: string): Link[];
|
|
|
13
13
|
* @param link 匹配到的链接对象
|
|
14
14
|
* @returns 处理后的标准格式对象
|
|
15
15
|
*/
|
|
16
|
-
export declare function process(ctx: Context, config: PluginConfig, link: Link): Promise<
|
|
16
|
+
export declare function process(ctx: Context, config: PluginConfig, link: Link): Promise<ParsedInfo | null>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// src/parsers/xiaohongshu.ts
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports.match = match;
|
|
4
5
|
exports.process = process;
|
|
5
|
-
const koishi_1 = require("koishi");
|
|
6
6
|
const cheerio_1 = require("cheerio");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
8
|
/**
|
|
@@ -11,7 +11,6 @@ const utils_1 = require("../utils");
|
|
|
11
11
|
* @returns 匹配到的链接对象数组
|
|
12
12
|
*/
|
|
13
13
|
function match(content) {
|
|
14
|
-
// 正则表达式匹配包含查询参数的完整URL
|
|
15
14
|
const urlRegex = /https?:\/\/(?:www\.xiaohongshu\.com\/discovery\/item\/[A-Za-z0-9]+|xhslink\.com\/[A-Za-z0-9]+)\??[^ \n\r]*/g;
|
|
16
15
|
const matches = content.match(urlRegex);
|
|
17
16
|
if (!matches)
|
|
@@ -19,7 +18,7 @@ function match(content) {
|
|
|
19
18
|
return matches.map(url => ({
|
|
20
19
|
platform: 'xiaohongshu',
|
|
21
20
|
type: 'note',
|
|
22
|
-
id: url.split('/').pop().split('?')[0],
|
|
21
|
+
id: url.split('/').pop().split('?')[0],
|
|
23
22
|
url: url
|
|
24
23
|
}));
|
|
25
24
|
}
|
|
@@ -35,25 +34,26 @@ async function process(ctx, config, link) {
|
|
|
35
34
|
// 步骤一:从原始分享链接中提取 xsec_token
|
|
36
35
|
let token = null;
|
|
37
36
|
try {
|
|
38
|
-
//
|
|
37
|
+
// 解码URL中的HTML实体, 主要是 & -> &
|
|
39
38
|
const decodedUrl = link.url.replace(/&/g, '&');
|
|
40
39
|
const originalUrl = new URL(decodedUrl);
|
|
41
40
|
token = originalUrl.searchParams.get('xsec_token');
|
|
42
|
-
if (token) {
|
|
41
|
+
if (token && config.logLevel === 'full') {
|
|
43
42
|
logger.info(`成功从分享链接中提取 xsec_token。`);
|
|
44
43
|
}
|
|
45
|
-
else {
|
|
46
|
-
logger.
|
|
44
|
+
else if (config.logLevel === 'full') {
|
|
45
|
+
logger.debug(`分享链接中未找到 xsec_token: ${link.url}`);
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
catch (e) {
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
if (config.logLevel === 'full')
|
|
50
|
+
logger.debug(`解析分享链接URL失败: ${link.url}`);
|
|
52
51
|
}
|
|
53
52
|
let finalUrl = link.url;
|
|
54
53
|
// 步骤二:如果是短链接,获取其跳转后的基础地址
|
|
55
54
|
if (link.url.includes('xhslink.com')) {
|
|
56
|
-
|
|
55
|
+
if (config.logLevel === 'full')
|
|
56
|
+
logger.info(`小红书短链接解析:尝试获取 ${link.url} 的最终地址`);
|
|
57
57
|
try {
|
|
58
58
|
const response = await ctx.http(link.url, {
|
|
59
59
|
method: 'GET',
|
|
@@ -63,14 +63,16 @@ async function process(ctx, config, link) {
|
|
|
63
63
|
const location = response.headers.get('location');
|
|
64
64
|
if (location) {
|
|
65
65
|
finalUrl = location;
|
|
66
|
-
|
|
66
|
+
if (config.logLevel === 'full')
|
|
67
|
+
logger.info(`短链接解析成功,跳转地址: ${finalUrl}`);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
catch (e) {
|
|
70
71
|
const location = e.response?.headers?.location;
|
|
71
72
|
if (location) {
|
|
72
73
|
finalUrl = location;
|
|
73
|
-
|
|
74
|
+
if (config.logLevel === 'full')
|
|
75
|
+
logger.info(`短链接解析成功,跳转地址: ${finalUrl}`);
|
|
74
76
|
}
|
|
75
77
|
else {
|
|
76
78
|
logger.error(`解析短链接时发生网络错误: ${e.message}`);
|
|
@@ -97,8 +99,8 @@ async function process(ctx, config, link) {
|
|
|
97
99
|
logger.error(`构建最终请求URL失败: ${finalUrl}`);
|
|
98
100
|
return null;
|
|
99
101
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
if (config.logLevel === 'full')
|
|
103
|
+
logger.info(`正在抓取小红书页面: ${urlToFetch}`);
|
|
102
104
|
try {
|
|
103
105
|
const html = await ctx.http.get(urlToFetch, {
|
|
104
106
|
headers: { 'User-Agent': config.userAgent }
|
|
@@ -109,61 +111,67 @@ async function process(ctx, config, link) {
|
|
|
109
111
|
logger.error('在页面中未找到 __INITIAL_STATE__ 数据块,可能是token无效或小红书策略变更。');
|
|
110
112
|
return null;
|
|
111
113
|
}
|
|
112
|
-
const jsonStr = scriptContent
|
|
113
|
-
.replace(/window\.__INITIAL_STATE__\s*=\s*/, '')
|
|
114
|
-
.replace(/undefined/g, 'null');
|
|
114
|
+
const jsonStr = scriptContent.replace(/window\.__INITIAL_STATE__\s*=\s*/, '').replace(/undefined/g, 'null');
|
|
115
115
|
const pageData = JSON.parse(jsonStr);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const noteKey = Object.keys(pageData.note.noteDetailMap)[0];
|
|
117
|
+
if (!noteKey) {
|
|
118
|
+
logger.error('无法在页面数据中找到笔记详情。');
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const noteData = pageData.note.noteDetailMap[noteKey].note;
|
|
122
|
+
// --- 构建结构化数据 ---
|
|
119
123
|
let videoUrl = null;
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
let coverUrl = undefined;
|
|
125
|
+
const images = [];
|
|
122
126
|
if (noteData.type === 'video' && noteData.video) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
if (config.logLevel === 'full') {
|
|
128
|
+
logger.info(`[XHS Video Debug] 发现视频笔记,视频数据对象: \n${JSON.stringify(noteData.video, null, 2)}`);
|
|
129
|
+
}
|
|
130
|
+
if (noteData.video.media?.stream?.h264?.[0]?.masterUrl) {
|
|
131
|
+
videoUrl = noteData.video.media.stream.h264[0].masterUrl;
|
|
132
|
+
if (config.logLevel === 'full') {
|
|
133
|
+
logger.info(`[XHS Video Debug] 已提取视频链接: ${videoUrl}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
logger.warn('[XHS Video Debug] 未能从预期路径 `note.video.media.stream.h264[0].masterUrl` 找到视频链接。');
|
|
138
|
+
}
|
|
139
|
+
if (noteData.imageList && noteData.imageList.length > 0) {
|
|
140
|
+
coverUrl = noteData.imageList[0].infoList.find(i => i.imageScene === 'WB_DETAIL_SHARE')?.url || noteData.imageList[0].infoList[1]?.url;
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
|
-
|
|
132
|
-
if (config.xhsAuthor) {
|
|
133
|
-
text += `UP主:${noteData.user.nickname}\n`;
|
|
134
|
-
}
|
|
135
|
-
// 4. 简介 (添加“简介:”前缀)
|
|
136
|
-
if (config.xhsDesc) {
|
|
137
|
-
text += `简介:${noteData.desc}\n`;
|
|
138
|
-
}
|
|
139
|
-
// 5. 图片列表 (仅图文笔记)
|
|
140
|
-
if (noteData.type === 'normal' && Array.isArray(noteData.imageList)) {
|
|
141
|
-
const images = [];
|
|
143
|
+
else if (noteData.type === 'normal' && Array.isArray(noteData.imageList)) {
|
|
142
144
|
noteData.imageList.forEach((img) => {
|
|
143
145
|
const imageUrl = img.infoList.find((i) => i.imageScene === 'WB_DETAIL_SHARE')?.url || img.infoList[1]?.url || img.url_default;
|
|
144
146
|
if (imageUrl) {
|
|
145
|
-
images.push(
|
|
147
|
+
images.push(imageUrl);
|
|
146
148
|
}
|
|
147
149
|
});
|
|
148
|
-
text += images.join('\n');
|
|
149
|
-
text += '\n'; // 在图片后添加一个换行
|
|
150
|
-
}
|
|
151
|
-
// 6. 互动数据 (去除emoji,使用“|”分割)
|
|
152
|
-
if (config.xhsStat && noteData.interactInfo) {
|
|
153
|
-
const likes = (0, utils_1.numeral)(parseInt(noteData.interactInfo.likedCount), config);
|
|
154
|
-
const favorites = (0, utils_1.numeral)(parseInt(noteData.interactInfo.collectedCount), config);
|
|
155
|
-
const comments = (0, utils_1.numeral)(parseInt(noteData.interactInfo.commentCount), config);
|
|
156
|
-
text += `点赞:${likes} | 收藏:${favorites} | 评论:${comments}`;
|
|
157
150
|
}
|
|
151
|
+
const stats = {
|
|
152
|
+
'点赞': (0, utils_1.numeral)(parseInt(noteData.interactInfo.likedCount), config),
|
|
153
|
+
'收藏': (0, utils_1.numeral)(parseInt(noteData.interactInfo.collectedCount), config),
|
|
154
|
+
'评论': (0, utils_1.numeral)(parseInt(noteData.interactInfo.commentCount), config),
|
|
155
|
+
};
|
|
156
|
+
let statsString = config.xiaohongshuStatsFormat;
|
|
157
|
+
Object.keys(stats).forEach(key => {
|
|
158
|
+
statsString = statsString.replace(`{${key}}`, stats[key]);
|
|
159
|
+
});
|
|
158
160
|
return {
|
|
159
|
-
|
|
161
|
+
platform: 'xiaohongshu',
|
|
162
|
+
title: noteData.title,
|
|
163
|
+
authorName: noteData.user.nickname,
|
|
164
|
+
description: noteData.desc.trim(),
|
|
165
|
+
coverUrl: coverUrl,
|
|
160
166
|
videoUrl: videoUrl,
|
|
161
|
-
duration: (videoUrl && noteData.video) ? noteData.video.media.duration : null,
|
|
167
|
+
duration: (videoUrl && noteData.video?.media?.duration) ? noteData.video.media.duration / 1000 : null,
|
|
168
|
+
sourceUrl: urlToFetch,
|
|
169
|
+
stats: statsString,
|
|
170
|
+
images: images.length > 0 ? images : undefined,
|
|
162
171
|
};
|
|
163
172
|
}
|
|
164
173
|
catch (error) {
|
|
165
|
-
|
|
166
|
-
logger.error(`抓取或解析小红书页面时失败: ${message}`);
|
|
174
|
+
logger.error(`抓取或解析小红书页面时失败: ${error.message}`);
|
|
167
175
|
return null;
|
|
168
176
|
}
|
|
169
177
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -4,37 +4,33 @@ export interface Link {
|
|
|
4
4
|
id: string;
|
|
5
5
|
url: string;
|
|
6
6
|
}
|
|
7
|
-
export interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export interface ParsedInfo {
|
|
8
|
+
platform: 'bilibili' | 'xiaohongshu';
|
|
9
|
+
title: string;
|
|
10
|
+
authorName: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
coverUrl?: string;
|
|
13
|
+
videoUrl?: string | null;
|
|
14
|
+
duration?: number | null;
|
|
15
|
+
sourceUrl: string;
|
|
16
|
+
stats: string;
|
|
17
|
+
images?: string[];
|
|
12
18
|
}
|
|
13
19
|
export interface PluginConfig {
|
|
14
|
-
linktextParsing: boolean;
|
|
15
|
-
VideoParsing_ToLink: '1' | '2' | '3' | '4' | '5';
|
|
16
20
|
Video_ClarityPriority: '1' | '2';
|
|
17
|
-
BVnumberParsing: boolean;
|
|
18
21
|
Maximumduration: number;
|
|
19
22
|
Maximumduration_tip: string;
|
|
20
23
|
MinimumTimeInterval: number;
|
|
21
24
|
waitTip_Switch: false | string;
|
|
25
|
+
format: string;
|
|
26
|
+
bilibiliStatsFormat: string;
|
|
27
|
+
xiaohongshuStatsFormat: string;
|
|
22
28
|
parseLimit: number;
|
|
23
29
|
useNumeral: boolean;
|
|
24
30
|
showError: boolean;
|
|
25
31
|
bVideoIDPreference: 'bv' | 'av';
|
|
26
|
-
bVideoImage: boolean;
|
|
27
|
-
bVideoOwner: boolean;
|
|
28
|
-
bVideoDesc: boolean;
|
|
29
|
-
bVideoStat: boolean;
|
|
30
|
-
bVideoExtraStat: boolean;
|
|
31
|
-
bVideoShowLink: boolean;
|
|
32
32
|
userAgent: string;
|
|
33
|
-
|
|
34
|
-
xhsCover: boolean;
|
|
35
|
-
xhsDesc: boolean;
|
|
36
|
-
xhsAuthor: boolean;
|
|
37
|
-
xhsStat: boolean;
|
|
33
|
+
logLevel: 'none' | 'link_only' | 'full';
|
|
38
34
|
}
|
|
39
35
|
export interface BilibiliVideoInfo {
|
|
40
36
|
data: {
|