koishi-plugin-share-links-analysis 0.1.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/LICENSE +21 -0
- package/README.md +17 -0
- package/lib/core.d.ts +16 -0
- package/lib/core.js +70 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +160 -0
- package/lib/parsers/bilibili.d.ts +16 -0
- package/lib/parsers/bilibili.js +177 -0
- package/lib/parsers/xiaohongshu.d.ts +16 -0
- package/lib/parsers/xiaohongshu.js +169 -0
- package/lib/types.d.ts +121 -0
- package/lib/types.js +3 -0
- package/lib/utils.d.ts +8 -0
- package/lib/utils.js +20 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 shangxue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# koishi-plugin-share-links-analysis
|
|
2
|
+
|
|
3
|
+
# 视频链接解析插件说明 📺✨
|
|
4
|
+
|
|
5
|
+
本插件为用户提供便捷的分享链接链接解析服务,让聊天体验更加丰富多彩。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 鸣谢 💖
|
|
10
|
+
|
|
11
|
+
特别鸣谢以下项目的支持:
|
|
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版本修改
|
package/lib/core.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Link, PluginConfig, ProcessedLink } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* 从文本中解析出所有支持的链接
|
|
5
|
+
* @param content 消息内容
|
|
6
|
+
* @returns 解析出的链接对象数组
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveLinks(content: string): Link[];
|
|
9
|
+
/**
|
|
10
|
+
* 处理单个链接,并返回格式化后的结果
|
|
11
|
+
* @param ctx Koishi Context
|
|
12
|
+
* @param config 插件配置
|
|
13
|
+
* @param link 解析出的链接对象
|
|
14
|
+
* @returns 处理后的链接结果,如果失败则返回 null
|
|
15
|
+
*/
|
|
16
|
+
export declare function processLink(ctx: Context, config: PluginConfig, link: Link): Promise<ProcessedLink | null>;
|
package/lib/core.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveLinks = resolveLinks;
|
|
37
|
+
exports.processLink = processLink;
|
|
38
|
+
const Bilibili = __importStar(require("./parsers/bilibili"));
|
|
39
|
+
const Xiaohongshu = __importStar(require("./parsers/xiaohongshu"));
|
|
40
|
+
// 定义所有支持的解析器
|
|
41
|
+
const parsers = [Bilibili, Xiaohongshu];
|
|
42
|
+
/**
|
|
43
|
+
* 从文本中解析出所有支持的链接
|
|
44
|
+
* @param content 消息内容
|
|
45
|
+
* @returns 解析出的链接对象数组
|
|
46
|
+
*/
|
|
47
|
+
function resolveLinks(content) {
|
|
48
|
+
const allLinks = [];
|
|
49
|
+
for (const parser of parsers) {
|
|
50
|
+
const links = parser.match(content);
|
|
51
|
+
allLinks.push(...links);
|
|
52
|
+
}
|
|
53
|
+
return allLinks;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 处理单个链接,并返回格式化后的结果
|
|
57
|
+
* @param ctx Koishi Context
|
|
58
|
+
* @param config 插件配置
|
|
59
|
+
* @param link 解析出的链接对象
|
|
60
|
+
* @returns 处理后的链接结果,如果失败则返回 null
|
|
61
|
+
*/
|
|
62
|
+
async function processLink(ctx, config, link) {
|
|
63
|
+
for (const parser of parsers) {
|
|
64
|
+
// 检查这个解析器是否能处理此类型的链接
|
|
65
|
+
if (parser.match(link.url).length > 0) {
|
|
66
|
+
return await parser.process(ctx, config, link);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
import { PluginConfig } from './types';
|
|
3
|
+
export declare const name = "share-links-analysis";
|
|
4
|
+
export declare const inject: {
|
|
5
|
+
required: string[];
|
|
6
|
+
optional: string[];
|
|
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\nB\u7AD9\u77ED\u94FE\u63A5(b23.tv)\u89E3\u6790\u9700\u8981 puppeteer \u670D\u52A1\u652F\u6301\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5\u5E76\u542F\u7528\u8BE5\u63D2\u4EF6\u3002\n";
|
|
9
|
+
export declare const Config: Schema<PluginConfig>;
|
|
10
|
+
export declare function apply(ctx: Context, config: PluginConfig): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = exports.usage = exports.inject = exports.name = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
const core_1 = require("./core");
|
|
7
|
+
// 这是插件的元数据
|
|
8
|
+
exports.name = 'share-links-analysis';
|
|
9
|
+
exports.inject = {
|
|
10
|
+
required: ['BiliBiliVideo'],
|
|
11
|
+
optional: ['puppeteer'], // 【修改】将 puppeteer 作为可选依赖注入
|
|
12
|
+
};
|
|
13
|
+
exports.usage = `
|
|
14
|
+
开启插件后,即可自动解析分享链接
|
|
15
|
+
向Bot发送B站、小红书等支持平台的分享链接,会返回图文信息与视频。
|
|
16
|
+
|
|
17
|
+
B站短链接(b23.tv)解析需要 puppeteer 服务支持,请确保已安装并启用该插件。
|
|
18
|
+
`;
|
|
19
|
+
exports.Config = koishi_1.Schema.intersect([
|
|
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
|
+
Video_ClarityPriority: koishi_1.Schema.union([
|
|
30
|
+
koishi_1.Schema.const('1').description('低清晰度优先(低清晰度的视频发得快一点)'),
|
|
31
|
+
koishi_1.Schema.const('2').description('高清晰度优先(建议在B站观看高画质视频)'),
|
|
32
|
+
]).role('radio').default('1').description("发送的视频清晰度优先策略"),
|
|
33
|
+
Maximumduration: koishi_1.Schema.number().default(25).description("允许解析的视频最大时长(分钟)`超过这个时长就不会发送视频`").min(1),
|
|
34
|
+
Maximumduration_tip: koishi_1.Schema.union([
|
|
35
|
+
koishi_1.Schema.const('不返回文字提示').description('不返回文字提示'),
|
|
36
|
+
koishi_1.Schema.string().description('返回文字提示(请在右侧填写文字内容)').default('视频太长啦!还是去B站看吧~'),
|
|
37
|
+
]).description("对过长视频的文字提示内容").default('视频太长啦!还是去B站看吧~'),
|
|
38
|
+
MinimumTimeInterval: koishi_1.Schema.number().default(180).description("若干`秒`内不再处理相同链接 `防止多bot互相触发导致的刷屏/性能浪费`").min(1),
|
|
39
|
+
waitTip_Switch: koishi_1.Schema.union([
|
|
40
|
+
koishi_1.Schema.const(false).description('不返回文字提示'),
|
|
41
|
+
koishi_1.Schema.string().description('返回文字提示(请在右侧填写文字内容)'),
|
|
42
|
+
]).description("是否返回等待提示。开启后,会发送`等待提示语`").default(false),
|
|
43
|
+
}).description("基础设置"),
|
|
44
|
+
koishi_1.Schema.object({
|
|
45
|
+
BVnumberParsing: koishi_1.Schema.boolean().default(true).description("是否允许根据`独立的BV号`解析视频 `开启后,可以通过视频的BV号解析视频。`"),
|
|
46
|
+
parseLimit: koishi_1.Schema.number().default(3).description("单对话多链接解析上限").hidden(),
|
|
47
|
+
useNumeral: koishi_1.Schema.boolean().default(true).description("使用格式化数字 (如 10000 -> 1万)").hidden(),
|
|
48
|
+
showError: koishi_1.Schema.boolean().default(false).description("当链接不正确时提醒发送者").hidden(),
|
|
49
|
+
bVideoIDPreference: koishi_1.Schema.union([
|
|
50
|
+
koishi_1.Schema.const("bv").description("BV 号"),
|
|
51
|
+
koishi_1.Schema.const("av").description("AV 号"),
|
|
52
|
+
]).default("bv").description("B站ID 偏好").hidden(),
|
|
53
|
+
bVideoImage: koishi_1.Schema.boolean().default(true).description("显示封面"),
|
|
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("小红书内容解析设置"),
|
|
66
|
+
koishi_1.Schema.object({
|
|
67
|
+
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
|
+
loggerinfo: koishi_1.Schema.boolean().default(false).description("日志调试输出 `日常使用无需开启`"),
|
|
69
|
+
}).description("调试设置"),
|
|
70
|
+
]);
|
|
71
|
+
// 主插件逻辑
|
|
72
|
+
function apply(ctx, config) {
|
|
73
|
+
const logger = ctx.logger('share-links-analysis');
|
|
74
|
+
const lastProcessedUrls = {};
|
|
75
|
+
ctx.middleware(async (session, next) => {
|
|
76
|
+
if (!session.content || !session.channelId) {
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
let content = session.content;
|
|
80
|
+
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
|
+
const links = (0, core_1.resolveLinks)(content);
|
|
90
|
+
if (links.length === 0)
|
|
91
|
+
return next();
|
|
92
|
+
let linkCount = 0;
|
|
93
|
+
for (const link of links) {
|
|
94
|
+
if (linkCount >= config.parseLimit) {
|
|
95
|
+
await session.send("已达到单次解析上限…");
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
if (!lastProcessedUrls[channelId]) {
|
|
100
|
+
lastProcessedUrls[channelId] = {};
|
|
101
|
+
}
|
|
102
|
+
if (now - (lastProcessedUrls[channelId][link.url] || 0) < config.MinimumTimeInterval * 1000) {
|
|
103
|
+
if (config.loggerinfo)
|
|
104
|
+
logger.info(`链接 ${link.url} 在冷却时间内,跳过处理。`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (config.waitTip_Switch) {
|
|
108
|
+
await session.send(config.waitTip_Switch);
|
|
109
|
+
}
|
|
110
|
+
const result = await (0, core_1.processLink)(ctx, config, link);
|
|
111
|
+
if (result) {
|
|
112
|
+
lastProcessedUrls[channelId][link.url] = now;
|
|
113
|
+
await sendResult(session, config, result, logger);
|
|
114
|
+
}
|
|
115
|
+
else if (config.showError) {
|
|
116
|
+
await session.send(`无法解析链接:${link.url}。可能是不支持的类型或链接有误。`);
|
|
117
|
+
}
|
|
118
|
+
linkCount++;
|
|
119
|
+
}
|
|
120
|
+
return next();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function sendResult(session, config, result, logger) {
|
|
124
|
+
if (config.linktextParsing && result.text) {
|
|
125
|
+
let message = result.text;
|
|
126
|
+
if (!config.bVideoShowLink && result.sourceUrl) {
|
|
127
|
+
message = message.replace(new RegExp(result.sourceUrl + '\\n?$'), '');
|
|
128
|
+
}
|
|
129
|
+
await session.send(koishi_1.h.quote(session.messageId) + message);
|
|
130
|
+
}
|
|
131
|
+
if (result.videoUrl && result.duration) {
|
|
132
|
+
if (result.duration > config.Maximumduration * 60) {
|
|
133
|
+
if (config.Maximumduration_tip && config.Maximumduration_tip !== '不返回文字提示') {
|
|
134
|
+
await session.send(config.Maximumduration_tip);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (result.videoUrl && config.VideoParsing_ToLink !== '1') {
|
|
140
|
+
switch (config.VideoParsing_ToLink) {
|
|
141
|
+
case '2':
|
|
142
|
+
await session.send(koishi_1.h.video(result.videoUrl));
|
|
143
|
+
break;
|
|
144
|
+
case '3':
|
|
145
|
+
await session.send(koishi_1.h.text(result.videoUrl));
|
|
146
|
+
break;
|
|
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
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (config.loggerinfo) {
|
|
158
|
+
logger.info(`解析结果: \n ${JSON.stringify(result, null, 2)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Link, ProcessedLink, PluginConfig } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* 在文本中匹配B站链接
|
|
5
|
+
* @param content 消息内容
|
|
6
|
+
* @returns 匹配到的链接对象数组
|
|
7
|
+
*/
|
|
8
|
+
export declare function match(content: string): Link[];
|
|
9
|
+
/**
|
|
10
|
+
* 处理单个B站链接
|
|
11
|
+
* @param ctx Koishi Context
|
|
12
|
+
* @param config 插件配置
|
|
13
|
+
* @param link 匹配到的链接对象
|
|
14
|
+
* @returns 处理后的标准格式对象
|
|
15
|
+
*/
|
|
16
|
+
export declare function process(ctx: Context, config: PluginConfig, link: Link): Promise<ProcessedLink | null>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.match = match;
|
|
4
|
+
exports.process = process;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
const utils_1 = require("../utils");
|
|
7
|
+
/**
|
|
8
|
+
* 在文本中匹配B站链接
|
|
9
|
+
* @param content 消息内容
|
|
10
|
+
* @returns 匹配到的链接对象数组
|
|
11
|
+
*/
|
|
12
|
+
function match(content) {
|
|
13
|
+
const linkRegex = [
|
|
14
|
+
{ pattern: /(https?:\/\/)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z]+)/i, type: "video" },
|
|
15
|
+
{ pattern: /(https?:\/\/)?b23\.tv\/([0-9a-zA-Z]+)/i, type: "short" },
|
|
16
|
+
];
|
|
17
|
+
const results = [];
|
|
18
|
+
for (const rule of linkRegex) {
|
|
19
|
+
const matches = [...content.matchAll(new RegExp(rule.pattern, 'gi'))];
|
|
20
|
+
for (const matchArr of matches) {
|
|
21
|
+
if (matchArr[2]) {
|
|
22
|
+
results.push({
|
|
23
|
+
platform: 'bilibili',
|
|
24
|
+
type: rule.type,
|
|
25
|
+
id: matchArr[2], // 捕获组2是ID
|
|
26
|
+
url: matchArr[0] // 完整匹配的URL
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 处理单个B站链接
|
|
35
|
+
* @param ctx Koishi Context
|
|
36
|
+
* @param config 插件配置
|
|
37
|
+
* @param link 匹配到的链接对象
|
|
38
|
+
* @returns 处理后的标准格式对象
|
|
39
|
+
*/
|
|
40
|
+
async function process(ctx, config, link) {
|
|
41
|
+
const logger = ctx.logger('share-links-analysis:bilibili');
|
|
42
|
+
let videoId = link.id;
|
|
43
|
+
let videoIdType = link.type;
|
|
44
|
+
// 如果是短链接,需要解析出真实ID
|
|
45
|
+
if (link.type === 'short') {
|
|
46
|
+
let finalUrl = '';
|
|
47
|
+
// 【方案一】优先使用原始插件的轻量级解析方案
|
|
48
|
+
logger.info(`B站短链接解析:尝试使用轻量级HTTP方案解析 ${link.url}`);
|
|
49
|
+
try {
|
|
50
|
+
const response = await ctx.http(link.url, {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
headers: { 'User-Agent': config.userAgent },
|
|
53
|
+
redirect: 'manual',
|
|
54
|
+
});
|
|
55
|
+
// 【修正】使用 .get('location') 的标准方法来获取响应头
|
|
56
|
+
const locationHeader = response.headers.get('location');
|
|
57
|
+
if (locationHeader) {
|
|
58
|
+
finalUrl = locationHeader;
|
|
59
|
+
logger.info(`轻量级方案成功 (方式A:获取响应头),短链接指向: ${finalUrl}`);
|
|
60
|
+
}
|
|
61
|
+
else if (response.data) {
|
|
62
|
+
const match = String(response.data).match(/<a\s+.*?href="([^"]*)"/i);
|
|
63
|
+
if (match && match[1]) {
|
|
64
|
+
finalUrl = match[1];
|
|
65
|
+
logger.info(`轻量级方案成功 (方式B:解析响应体),短链接指向: ${finalUrl}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
// 正常情况下这里不应再触发,仅作为网络错误等意外情况的捕获
|
|
71
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
72
|
+
logger.error(`轻量级HTTP请求失败: ${message}`);
|
|
73
|
+
}
|
|
74
|
+
// 【方案二】如果轻量级方案失败,并且安装了Puppeteer,则使用它作为后备
|
|
75
|
+
if (!finalUrl && ctx.puppeteer) {
|
|
76
|
+
logger.warn(`轻量级方案解析失败,切换至Puppeteer后备方案: ${link.url}`);
|
|
77
|
+
let page = null;
|
|
78
|
+
try {
|
|
79
|
+
page = await ctx.puppeteer.page();
|
|
80
|
+
await page.setUserAgent(config.userAgent);
|
|
81
|
+
await page.goto(link.url, { waitUntil: 'networkidle0' });
|
|
82
|
+
finalUrl = page.url(); // 获取跳转后的最终URL
|
|
83
|
+
logger.info(`Puppeteer方案成功,短链接指向: ${finalUrl}`);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
87
|
+
logger.error(`Puppeteer方案解析短链接 ${link.url} 失败: ${message}`);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (page)
|
|
92
|
+
await page.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (!finalUrl && !ctx.puppeteer) {
|
|
96
|
+
logger.error('轻量级方案解析短链接失败,且未安装或启用Puppeteer服务,无法继续解析。');
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// 从最终解析出的URL里提取视频ID
|
|
100
|
+
const matchedLinks = match(finalUrl);
|
|
101
|
+
if (matchedLinks.length > 0 && matchedLinks[0].type === 'video') {
|
|
102
|
+
videoId = matchedLinks[0].id;
|
|
103
|
+
videoIdType = 'video';
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
logger.warn(`在最终链接中未找到有效的视频ID: ${finalUrl}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 如果最终没能得到视频ID,则解析失败
|
|
111
|
+
if (videoIdType !== 'video' || !videoId) {
|
|
112
|
+
logger.warn(`无法从链接 ${link.url} 中解析出有效的B站视频ID。`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
logger.info(`获取视频信息,ID: ${videoId}`);
|
|
116
|
+
const idType = videoId.startsWith('BV') ? 'bvid' : 'aid';
|
|
117
|
+
const infoUrl = `https://api.bilibili.com/x/web-interface/view?${idType}=${videoId}`;
|
|
118
|
+
let info;
|
|
119
|
+
try {
|
|
120
|
+
info = await ctx.http.get(infoUrl, {
|
|
121
|
+
headers: { 'User-Agent': config.userAgent }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
+
logger.error(`获取B站视频信息失败,ID ${videoId}: ${message}`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!info || !info.data) {
|
|
130
|
+
logger.warn(`B站API未返回有效数据,ID ${videoId}。响应: ${JSON.stringify(info)}`);
|
|
131
|
+
return null;
|
|
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}`);
|
|
156
|
+
try {
|
|
157
|
+
const videoStream = await ctx.BiliBiliVideo.getBilibiliVideoStream(data.aid, data.bvid, data.pages[0].cid, config.Video_ClarityPriority === '1' ? 32 : 80, 'html5', 1);
|
|
158
|
+
if (videoStream?.data?.durl?.[0]?.url) {
|
|
159
|
+
videoUrl = videoStream.data.durl[0].url;
|
|
160
|
+
logger.info(`成功获取视频流,bvid: ${data.bvid}`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
logger.warn(`未能获取视频流,bvid: ${data.bvid}。API返回的流数据无效。响应: ${JSON.stringify(videoStream)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
168
|
+
logger.error(`通过BiliBiliVideo服务获取视频流失败,bvid: ${data.bvid}: ${message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
text: text.trim(),
|
|
173
|
+
videoUrl: videoUrl,
|
|
174
|
+
duration: data.duration,
|
|
175
|
+
sourceUrl: sourceUrl,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Link, ProcessedLink, PluginConfig } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* 在文本中匹配小红书链接 (长链接或短链接)
|
|
5
|
+
* @param content 消息内容
|
|
6
|
+
* @returns 匹配到的链接对象数组
|
|
7
|
+
*/
|
|
8
|
+
export declare function match(content: string): Link[];
|
|
9
|
+
/**
|
|
10
|
+
* 处理单个小红书链接
|
|
11
|
+
* @param ctx Koishi Context
|
|
12
|
+
* @param config 插件配置
|
|
13
|
+
* @param link 匹配到的链接对象
|
|
14
|
+
* @returns 处理后的标准格式对象
|
|
15
|
+
*/
|
|
16
|
+
export declare function process(ctx: Context, config: PluginConfig, link: Link): Promise<ProcessedLink | null>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.match = match;
|
|
4
|
+
exports.process = process;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
const cheerio_1 = require("cheerio");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
/**
|
|
9
|
+
* 在文本中匹配小红书链接 (长链接或短链接)
|
|
10
|
+
* @param content 消息内容
|
|
11
|
+
* @returns 匹配到的链接对象数组
|
|
12
|
+
*/
|
|
13
|
+
function match(content) {
|
|
14
|
+
// 正则表达式匹配包含查询参数的完整URL
|
|
15
|
+
const urlRegex = /https?:\/\/(?:www\.xiaohongshu\.com\/discovery\/item\/[A-Za-z0-9]+|xhslink\.com\/[A-Za-z0-9]+)\??[^ \n\r]*/g;
|
|
16
|
+
const matches = content.match(urlRegex);
|
|
17
|
+
if (!matches)
|
|
18
|
+
return [];
|
|
19
|
+
return matches.map(url => ({
|
|
20
|
+
platform: 'xiaohongshu',
|
|
21
|
+
type: 'note',
|
|
22
|
+
id: url.split('/').pop().split('?')[0], // 获取纯ID
|
|
23
|
+
url: url
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 处理单个小红书链接
|
|
28
|
+
* @param ctx Koishi Context
|
|
29
|
+
* @param config 插件配置
|
|
30
|
+
* @param link 匹配到的链接对象
|
|
31
|
+
* @returns 处理后的标准格式对象
|
|
32
|
+
*/
|
|
33
|
+
async function process(ctx, config, link) {
|
|
34
|
+
const logger = ctx.logger('share-links-analysis:xiaohongshu');
|
|
35
|
+
// 步骤一:从原始分享链接中提取 xsec_token
|
|
36
|
+
let token = null;
|
|
37
|
+
try {
|
|
38
|
+
// 【修正】解码URL中的HTML实体, 主要是 & -> &
|
|
39
|
+
const decodedUrl = link.url.replace(/&/g, '&');
|
|
40
|
+
const originalUrl = new URL(decodedUrl);
|
|
41
|
+
token = originalUrl.searchParams.get('xsec_token');
|
|
42
|
+
if (token) {
|
|
43
|
+
logger.info(`成功从分享链接中提取 xsec_token。`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
logger.warn(`分享链接中未找到 xsec_token: ${link.url}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
logger.warn(`解析分享链接URL失败: ${link.url}`);
|
|
51
|
+
// 即使URL解析失败,也继续尝试,因为短链接可能没有参数
|
|
52
|
+
}
|
|
53
|
+
let finalUrl = link.url;
|
|
54
|
+
// 步骤二:如果是短链接,获取其跳转后的基础地址
|
|
55
|
+
if (link.url.includes('xhslink.com')) {
|
|
56
|
+
logger.info(`小红书短链接解析:尝试获取 ${link.url} 的最终地址`);
|
|
57
|
+
try {
|
|
58
|
+
const response = await ctx.http(link.url, {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
headers: { 'User-Agent': config.userAgent },
|
|
61
|
+
redirect: 'manual',
|
|
62
|
+
});
|
|
63
|
+
const location = response.headers.get('location');
|
|
64
|
+
if (location) {
|
|
65
|
+
finalUrl = location;
|
|
66
|
+
logger.info(`短链接解析成功,跳转地址: ${finalUrl}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
const location = e.response?.headers?.location;
|
|
71
|
+
if (location) {
|
|
72
|
+
finalUrl = location;
|
|
73
|
+
logger.info(`短链接解析成功,跳转地址: ${finalUrl}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
logger.error(`解析短链接时发生网络错误: ${e.message}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 步骤三:构建最终要抓取的URL
|
|
82
|
+
let urlToFetch;
|
|
83
|
+
try {
|
|
84
|
+
const baseUrl = finalUrl.split('?')[0];
|
|
85
|
+
if (token) {
|
|
86
|
+
// 如果有token,构建一个只带token的纯净URL
|
|
87
|
+
const targetUrl = new URL(baseUrl);
|
|
88
|
+
targetUrl.searchParams.set('xsec_token', token);
|
|
89
|
+
urlToFetch = targetUrl.toString();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// 【修正】如果没有token,则直接尝试访问原始最终链接
|
|
93
|
+
urlToFetch = finalUrl;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
logger.error(`构建最终请求URL失败: ${finalUrl}`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// 步骤四:获取最终页面的HTML并解析
|
|
101
|
+
logger.info(`正在抓取小红书页面: ${urlToFetch}`);
|
|
102
|
+
try {
|
|
103
|
+
const html = await ctx.http.get(urlToFetch, {
|
|
104
|
+
headers: { 'User-Agent': config.userAgent }
|
|
105
|
+
});
|
|
106
|
+
const $ = (0, cheerio_1.load)(html);
|
|
107
|
+
const scriptContent = $('script:contains("window.__INITIAL_STATE__")').html();
|
|
108
|
+
if (!scriptContent) {
|
|
109
|
+
logger.error('在页面中未找到 __INITIAL_STATE__ 数据块,可能是token无效或小红书策略变更。');
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const jsonStr = scriptContent
|
|
113
|
+
.replace(/window\.__INITIAL_STATE__\s*=\s*/, '')
|
|
114
|
+
.replace(/undefined/g, 'null');
|
|
115
|
+
const pageData = JSON.parse(jsonStr);
|
|
116
|
+
const noteData = Object.values(pageData.note.noteDetailMap)[0].note;
|
|
117
|
+
// 【修改】完全重构图文消息的构建逻辑,严格对齐B站风格
|
|
118
|
+
let text = '';
|
|
119
|
+
let videoUrl = null;
|
|
120
|
+
// 1. 标题 (去除方括号)
|
|
121
|
+
text += `${noteData.title}\n`;
|
|
122
|
+
if (noteData.type === 'video' && noteData.video) {
|
|
123
|
+
videoUrl = noteData.video.media.stream.h264[0].masterUrl;
|
|
124
|
+
// 2. 封面 (仅视频笔记)
|
|
125
|
+
if (config.xhsCover && noteData.imageList && noteData.imageList.length > 0) {
|
|
126
|
+
const coverUrl = noteData.imageList[0].infoList.find(i => i.imageScene === 'WB_DETAIL_SHARE')?.url || noteData.imageList[0].infoList[1]?.url;
|
|
127
|
+
if (coverUrl)
|
|
128
|
+
text += koishi_1.h.image(coverUrl) + '\n';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// 3. 作者信息 (统一使用“UP主”)
|
|
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 = [];
|
|
142
|
+
noteData.imageList.forEach((img) => {
|
|
143
|
+
const imageUrl = img.infoList.find((i) => i.imageScene === 'WB_DETAIL_SHARE')?.url || img.infoList[1]?.url || img.url_default;
|
|
144
|
+
if (imageUrl) {
|
|
145
|
+
images.push(koishi_1.h.image(imageUrl).toString());
|
|
146
|
+
}
|
|
147
|
+
});
|
|
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
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
text: text.trim(),
|
|
160
|
+
videoUrl: videoUrl,
|
|
161
|
+
duration: (videoUrl && noteData.video) ? noteData.video.media.duration : null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
logger.error(`抓取或解析小红书页面时失败: ${message}`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export interface Link {
|
|
2
|
+
platform: 'bilibili' | 'xiaohongshu';
|
|
3
|
+
type: string;
|
|
4
|
+
id: string;
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProcessedLink {
|
|
8
|
+
text: string;
|
|
9
|
+
videoUrl: string | null;
|
|
10
|
+
duration: number | null;
|
|
11
|
+
sourceUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PluginConfig {
|
|
14
|
+
linktextParsing: boolean;
|
|
15
|
+
VideoParsing_ToLink: '1' | '2' | '3' | '4' | '5';
|
|
16
|
+
Video_ClarityPriority: '1' | '2';
|
|
17
|
+
BVnumberParsing: boolean;
|
|
18
|
+
Maximumduration: number;
|
|
19
|
+
Maximumduration_tip: string;
|
|
20
|
+
MinimumTimeInterval: number;
|
|
21
|
+
waitTip_Switch: false | string;
|
|
22
|
+
parseLimit: number;
|
|
23
|
+
useNumeral: boolean;
|
|
24
|
+
showError: boolean;
|
|
25
|
+
bVideoIDPreference: 'bv' | 'av';
|
|
26
|
+
bVideoImage: boolean;
|
|
27
|
+
bVideoOwner: boolean;
|
|
28
|
+
bVideoDesc: boolean;
|
|
29
|
+
bVideoStat: boolean;
|
|
30
|
+
bVideoExtraStat: boolean;
|
|
31
|
+
bVideoShowLink: boolean;
|
|
32
|
+
userAgent: string;
|
|
33
|
+
loggerinfo: boolean;
|
|
34
|
+
xhsCover: boolean;
|
|
35
|
+
xhsDesc: boolean;
|
|
36
|
+
xhsAuthor: boolean;
|
|
37
|
+
xhsStat: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface BilibiliVideoInfo {
|
|
40
|
+
data: {
|
|
41
|
+
bvid: string;
|
|
42
|
+
aid: number;
|
|
43
|
+
videos: number;
|
|
44
|
+
pic: string;
|
|
45
|
+
title: string;
|
|
46
|
+
pubdate: number;
|
|
47
|
+
ctime: number;
|
|
48
|
+
desc: string;
|
|
49
|
+
duration: number;
|
|
50
|
+
owner: {
|
|
51
|
+
mid: number;
|
|
52
|
+
name: string;
|
|
53
|
+
face: string;
|
|
54
|
+
};
|
|
55
|
+
stat: {
|
|
56
|
+
aid: number;
|
|
57
|
+
view: number;
|
|
58
|
+
danmaku: number;
|
|
59
|
+
reply: number;
|
|
60
|
+
favorite: number;
|
|
61
|
+
coin: number;
|
|
62
|
+
share: number;
|
|
63
|
+
like: number;
|
|
64
|
+
};
|
|
65
|
+
pages: {
|
|
66
|
+
cid: number;
|
|
67
|
+
page: number;
|
|
68
|
+
part: string;
|
|
69
|
+
duration: number;
|
|
70
|
+
}[];
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
declare module 'koishi' {
|
|
74
|
+
interface Context {
|
|
75
|
+
BiliBiliVideo: any;
|
|
76
|
+
puppeteer?: any;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export interface XhsImageInfo {
|
|
80
|
+
imageScene: string;
|
|
81
|
+
url: string;
|
|
82
|
+
}
|
|
83
|
+
export interface XhsImage {
|
|
84
|
+
infoList: XhsImageInfo[];
|
|
85
|
+
url_default: string;
|
|
86
|
+
}
|
|
87
|
+
export interface XhsNoteData {
|
|
88
|
+
title: string;
|
|
89
|
+
desc: string;
|
|
90
|
+
type: 'video' | 'normal';
|
|
91
|
+
user: {
|
|
92
|
+
nickname: string;
|
|
93
|
+
avatar: string;
|
|
94
|
+
};
|
|
95
|
+
interactInfo: {
|
|
96
|
+
likedCount: string;
|
|
97
|
+
collectedCount: string;
|
|
98
|
+
commentCount: string;
|
|
99
|
+
shareCount: string;
|
|
100
|
+
};
|
|
101
|
+
imageList?: XhsImage[];
|
|
102
|
+
video?: {
|
|
103
|
+
media: {
|
|
104
|
+
duration: number;
|
|
105
|
+
stream: {
|
|
106
|
+
h264: {
|
|
107
|
+
masterUrl: string;
|
|
108
|
+
}[];
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export interface XhsInitialState {
|
|
114
|
+
note: {
|
|
115
|
+
noteDetailMap: {
|
|
116
|
+
[key: string]: {
|
|
117
|
+
note: XhsNoteData;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
}
|
package/lib/types.js
ADDED
package/lib/utils.d.ts
ADDED
package/lib/utils.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.numeral = numeral;
|
|
4
|
+
/**
|
|
5
|
+
* 将数字格式化为易读的字符串(如 万、亿)
|
|
6
|
+
* @param num 数字
|
|
7
|
+
* @param config 插件配置
|
|
8
|
+
* @returns 格式化后的字符串
|
|
9
|
+
*/
|
|
10
|
+
function numeral(num, config) {
|
|
11
|
+
if (config.useNumeral) {
|
|
12
|
+
if (num >= 100000000) {
|
|
13
|
+
return (num / 100000000).toFixed(1) + "亿";
|
|
14
|
+
}
|
|
15
|
+
if (num >= 10000) {
|
|
16
|
+
return (num / 10000).toFixed(1) + "万";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return String(num);
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-share-links-analysis",
|
|
3
|
+
"description": "自用插件",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"typings": "lib/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/furryaxw/sharelinks-analysis.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/furryaxw/sharelinks-analysis",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/furryaxw/sharelinks-analysis/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"chatbot",
|
|
22
|
+
"koishi",
|
|
23
|
+
"plugin"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"koishi": "^4.16.8",
|
|
27
|
+
"koishi-plugin-bilibili-login": "^0.1.50"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"cheerio": "^1.0.0-rc.12"
|
|
31
|
+
}
|
|
32
|
+
}
|