koishi-plugin-share-links-analysis 0.3.0 → 0.5.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.
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ // src/parsers/twitter.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.match = match;
5
+ exports.process = process;
6
+ const koishi_1 = require("koishi");
7
+ const utils_1 = require("../utils");
8
+ // ======================
9
+ // 链接匹配规则
10
+ // ======================
11
+ const linkRules = [
12
+ {
13
+ pattern: /https?:\/\/(?:www\.)?(?:twitter\.com|x\.com|mobile\.twitter\.com)\/([\w-]+)\/status\/(\d+)/gi,
14
+ type: "tweet",
15
+ },
16
+ {
17
+ pattern: /https?:\/\/t\.co\/([a-zA-Z0-9]+)/gi,
18
+ type: "short",
19
+ }
20
+ ];
21
+ /**
22
+ * 在文本中匹配 Twitter/X 链接
23
+ */
24
+ function match(content) {
25
+ const results = [];
26
+ const seen = new Set();
27
+ for (const rule of linkRules) {
28
+ let match;
29
+ while ((match = rule.pattern.exec(content)) !== null) {
30
+ const username = match[1] || 'unknown';
31
+ const tweetId = match[2] || match[1];
32
+ let cleanUrl = match[0];
33
+ // 处理短链接
34
+ if (rule.type === 'short') {
35
+ cleanUrl = `https://t.co/${tweetId}`;
36
+ // 标准化域名
37
+ }
38
+ else {
39
+ cleanUrl = `https://x.com/${username}/status/${tweetId}`;
40
+ }
41
+ if (seen.has(cleanUrl))
42
+ continue;
43
+ seen.add(cleanUrl);
44
+ results.push({
45
+ platform: 'twitter',
46
+ type: rule.type,
47
+ id: tweetId,
48
+ url: cleanUrl,
49
+ });
50
+ }
51
+ }
52
+ return results;
53
+ }
54
+ /**
55
+ * 处理单条 Twitter 链接
56
+ */
57
+ async function process(ctx, config, link, session) {
58
+ const logger = ctx.logger('twitter:process');
59
+ let apiUrl;
60
+ if (link.type === 'short') {
61
+ // 短链接需先解析,但 vxtwitter 支持直接转换
62
+ apiUrl = `https://api.vxtwitter.com/tweet?id=${link.id}`;
63
+ }
64
+ // 标准推文链接
65
+ apiUrl = link.url.replace("x.com", "api.vxtwitter.com");
66
+ if (!apiUrl) {
67
+ logger.warn(`无效的 Twitter 链接: ${link.url}`);
68
+ await session.send('无法解析此 Twitter 链接,URL 格式不正确');
69
+ return null;
70
+ }
71
+ try {
72
+ logger.info(`🔍 解析推文: ${apiUrl}`);
73
+ const tweetData = await ctx.http.get(apiUrl, {
74
+ headers: {
75
+ 'User-Agent': config.userAgent,
76
+ 'Accept': 'application/json'
77
+ }
78
+ });
79
+ // 处理 API 错误
80
+ if (tweetData?.error) {
81
+ logger.error(`API 错误: ${tweetData.error}`);
82
+ await session.send(`Twitter API 错误: ${tweetData.error}`);
83
+ return null;
84
+ }
85
+ if (tweetData.possibly_sensitive && !config.allow_sensitive) {
86
+ await session.send('潜在的不合规内容,已停止发送');
87
+ return null;
88
+ }
89
+ // 解析媒体
90
+ let media;
91
+ if (tweetData?.hasMedia) {
92
+ media = parseMedia(tweetData);
93
+ }
94
+ const likes = (0, utils_1.numeral)(parseInt(tweetData.likes), config);
95
+ const replies = (0, utils_1.numeral)(parseInt(tweetData.replies), config);
96
+ const retweets = (0, utils_1.numeral)(parseInt(tweetData.retweets), config);
97
+ const statsString = `点赞: ${likes} | 评论: ${replies} | 转发: ${retweets}`;
98
+ let tweet_text;
99
+ if (tweetData.replyingTo) {
100
+ tweet_text = "回复:" + tweetData.text;
101
+ }
102
+ else {
103
+ tweet_text = tweetData.text;
104
+ }
105
+ const image = media?.images ? media?.images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
106
+ const mainbody = (0, utils_1.escapeHtml)(tweet_text) + image;
107
+ return {
108
+ platform: 'twitter',
109
+ title: `@${tweetData.user_screen_name} 的推文`,
110
+ authorName: tweetData.user_name || tweetData.user_screen_name,
111
+ mainbody: mainbody,
112
+ sourceUrl: link.url,
113
+ stats: statsString,
114
+ videoUrl: media?.videos[0]?.url, // 取第一个视频
115
+ coverUrl: media?.videos[0]?.preview_url,
116
+ };
117
+ }
118
+ catch (error) {
119
+ logger.error(`解析失败: ${error.message || error}`);
120
+ // 专项错误处理
121
+ if (error.message?.includes('429')) {
122
+ await session.send('Twitter API 速率限制,请稍后再试');
123
+ }
124
+ else if (error.message?.includes('404')) {
125
+ await session.send('推文不存在或已删除');
126
+ }
127
+ else if (error.message?.includes('ECONNRESET') || error.message?.includes('ETIMEDOUT')) {
128
+ await session.send('连接 Twitter API 超时,请重试');
129
+ }
130
+ else {
131
+ await session.send(`解析失败: ${error.message}`);
132
+ }
133
+ return null;
134
+ }
135
+ }
136
+ // ======================
137
+ // 内部工具函数
138
+ // ======================
139
+ /**
140
+ * 解析媒体数据
141
+ */
142
+ function parseMedia(tweetData) {
143
+ const images = [];
144
+ const videos = [];
145
+ for (const media of tweetData.media_extended) {
146
+ switch (media.type) {
147
+ case 'image':
148
+ images.push(media.url);
149
+ continue;
150
+ case 'video':
151
+ videos.push({
152
+ url: media.url,
153
+ preview_url: media.thumbnail_url,
154
+ duration: media.duration_millis ? media.duration_millis / 1000 : undefined
155
+ });
156
+ continue;
157
+ }
158
+ }
159
+ return { images, videos };
160
+ }
@@ -11,7 +11,7 @@ export declare function match(content: string): Link[];
11
11
  * @param ctx - Koishi Context
12
12
  * @param config
13
13
  */
14
- export declare function refreshXhsCookie(ctx: Context, config: PluginConfig): Promise<boolean>;
14
+ export declare function init(ctx: Context, config: PluginConfig): Promise<boolean>;
15
15
  /**
16
16
  * 处理单个小红书链接
17
17
  * @param ctx Koishi Context
@@ -2,10 +2,25 @@
2
2
  // src/parsers/xiaohongshu.ts
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.match = match;
5
- exports.refreshXhsCookie = refreshXhsCookie;
5
+ exports.init = init;
6
6
  exports.process = process;
7
+ const koishi_1 = require("koishi");
7
8
  const cheerio_1 = require("cheerio");
8
9
  const utils_1 = require("../utils");
10
+ const linkRules = [
11
+ {
12
+ pattern: /(?:https?:\/\/)?(?:www\.xiaohongshu\.com\/discovery\/item\/)([\w?=&\-.%]+)/gi,
13
+ type: "discovery",
14
+ },
15
+ {
16
+ pattern: /(?:https?:\/\/)?(?:www\.xiaohongshu\.com\/explore\/)([\w?=&\-.%]+)/gi,
17
+ type: "explore",
18
+ },
19
+ {
20
+ pattern: /(?:https?:\/\/)?(?:xhslink\.com\/(?:\w\/)?)([0-9a-zA-Z]+)/gi,
21
+ type: "short",
22
+ },
23
+ ];
9
24
  /**
10
25
  * 在文本中匹配小红书链接 (长链接或短链接)
11
26
  * @param content 消息内容
@@ -13,25 +28,28 @@ const utils_1 = require("../utils");
13
28
  */
14
29
  function match(content) {
15
30
  const results = [];
16
- // 匹配标准长链接和短链接
17
- const linkRegex = [
18
- { pattern: /(https?:\/\/)?(www\.xiaohongshu\.com\/discovery\/item\/([\w?=\-&\.%]+))/g, type: "discovery" },
19
- { pattern: /(https?:\/\/)?(www\.xiaohongshu\.com\/explore\/([\w?=\-&\.%]+))/g, type: "explore" },
20
- { pattern: /(https?:\/\/)?(xhslink\.com\/(m\/)?[0-9a-zA-Z]+)/g, type: "short" },
21
- ];
22
- for (const rule of linkRegex) {
23
- const matches = [...content.matchAll(new RegExp(rule.pattern, 'gi'))];
24
- for (const matchArr of matches) {
25
- if (matchArr[2]) {
26
- // 强制将所有链接格式化为 https 开头
27
- const formattedUrl = `https://${matchArr[2]}`;
28
- results.push({
29
- platform: 'xiaohongshu',
30
- type: rule.type,
31
- id: formattedUrl.split('/').pop().split('?')[0],
32
- url: formattedUrl
33
- });
34
- }
31
+ const seen = new Set();
32
+ for (const { pattern, type } of linkRules) {
33
+ let match;
34
+ while ((match = pattern.exec(content)) !== null) {
35
+ const idPart = match[1];
36
+ if (!idPart)
37
+ continue;
38
+ const cleanId = idPart.split('?')[0];
39
+ const host = type === "short" ? "xhslink.com" : "www.xiaohongshu.com";
40
+ const pathPrefix = type === "short"
41
+ ? (idPart.startsWith('m/') ? 'm/' : '')
42
+ : (type === "discovery" ? "discovery/item/" : "explore/");
43
+ const url = `https://${host}/${pathPrefix}${idPart}`;
44
+ if (seen.has(url))
45
+ continue;
46
+ seen.add(url);
47
+ results.push({
48
+ platform: 'xiaohongshu',
49
+ type,
50
+ id: cleanId,
51
+ url,
52
+ });
35
53
  }
36
54
  }
37
55
  return results;
@@ -41,7 +59,7 @@ function match(content) {
41
59
  * @param ctx - Koishi Context
42
60
  * @param config
43
61
  */
44
- async function refreshXhsCookie(ctx, config) {
62
+ async function init(ctx, config) {
45
63
  const logger = ctx.logger('share-links-analysis:xiaohongshu');
46
64
  const platformId = 'xiaohongshu';
47
65
  if (!ctx.puppeteer) {
@@ -239,26 +257,21 @@ async function process(ctx, config, link, session) {
239
257
  }
240
258
  });
241
259
  }
242
- const stats = {
243
- '点赞': (0, utils_1.numeral)(parseInt(noteData.interactInfo.likedCount), config),
244
- '收藏': (0, utils_1.numeral)(parseInt(noteData.interactInfo.collectedCount), config),
245
- '评论': (0, utils_1.numeral)(parseInt(noteData.interactInfo.commentCount), config),
246
- };
247
- let statsString = config.xiaohongshuStatsFormat;
248
- Object.keys(stats).forEach(key => {
249
- statsString = statsString.replace(`{${key}}`, stats[key]);
250
- });
260
+ const liked = (0, utils_1.numeral)(parseInt(noteData.interactInfo.likedCount), config);
261
+ const collected = (0, utils_1.numeral)(parseInt(noteData.interactInfo.collectedCount), config);
262
+ const comment = (0, utils_1.numeral)(parseInt(noteData.interactInfo.commentCount), config);
263
+ const statsString = `点赞: ${liked} | 收藏: ${collected} | 评论: ${comment}`;
264
+ const image = images ? images.map(img => koishi_1.h.image(img).toString()).join('\n') : '';
265
+ const mainbody = (0, utils_1.escapeHtml)(noteData.desc.trim()) + image;
251
266
  return {
252
267
  platform: 'xiaohongshu',
253
268
  title: noteData.title,
254
269
  authorName: noteData.user.nickname,
255
- description: noteData.desc.trim(),
270
+ mainbody: mainbody,
256
271
  coverUrl: coverUrl,
257
272
  videoUrl: videoUrl,
258
- duration: (videoUrl && noteData.video?.media?.duration) ? noteData.video.media.duration / 1000 : null,
259
273
  sourceUrl: urlToFetch,
260
274
  stats: statsString,
261
- images: images.length > 0 ? images : undefined,
262
275
  };
263
276
  }
264
277
  catch (error) {
package/lib/types.d.ts CHANGED
@@ -1,35 +1,34 @@
1
1
  export interface Link {
2
- platform: 'bilibili' | 'xiaohongshu';
2
+ platform: string;
3
3
  type: string;
4
4
  id: string;
5
5
  url: string;
6
6
  }
7
7
  export interface ParsedInfo {
8
- platform: 'bilibili' | 'xiaohongshu';
8
+ platform: string;
9
9
  title: string;
10
10
  authorName: string;
11
- description?: string;
11
+ mainbody?: string;
12
12
  coverUrl?: string;
13
13
  videoUrl?: string | null;
14
- duration?: number | null;
15
14
  sourceUrl: string;
16
15
  stats: string;
17
- images?: string[];
18
16
  }
19
17
  export interface PluginConfig {
20
18
  Video_ClarityPriority: '1' | '2';
21
- Maximumduration: number;
22
- Maximumduration_tip: string;
23
- MinimumTimeInterval: number;
19
+ Max_size: number;
20
+ Max_size_tip: string;
21
+ Min_Interval: number;
24
22
  waitTip_Switch: false | string;
25
- useForward: boolean;
23
+ useForward: 'plain' | 'forward' | 'mixed';
26
24
  format: string;
27
- bilibiliStatsFormat: string;
28
- xiaohongshuStatsFormat: string;
29
25
  parseLimit: number;
30
26
  useNumeral: boolean;
31
27
  showError: boolean;
32
- bVideoIDPreference: 'bv' | 'av';
28
+ allow_sensitive: boolean;
29
+ proxy: string;
30
+ onebotReadDir: string;
31
+ localDownloadDir: string;
33
32
  userAgent: string;
34
33
  logLevel: 'none' | 'link_only' | 'full';
35
34
  }
@@ -72,12 +71,6 @@ declare module 'koishi' {
72
71
  BiliBiliVideo: any;
73
72
  puppeteer?: any;
74
73
  }
75
- interface Tables {
76
- sla_cookie_cache: {
77
- platform: string;
78
- cookie: string;
79
- };
80
- }
81
74
  }
82
75
  export interface XhsImageInfo {
83
76
  imageScene: string;
package/lib/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { PluginConfig } from './types';
1
+ import { ParsedInfo, PluginConfig } from './types';
2
+ import { Logger, Session } from "koishi";
2
3
  /**
3
4
  * 将数字格式化为易读的字符串(如 万、亿)
4
5
  * @param num 数字
@@ -6,3 +7,7 @@ import { PluginConfig } from './types';
6
7
  * @returns 格式化后的字符串
7
8
  */
8
9
  export declare function numeral(num: number, config: PluginConfig): string;
10
+ export declare function escapeHtml(str: string): string;
11
+ export declare function getFileSize(url: string, proxy: string | undefined, userAgent: string | undefined): Promise<number | null>;
12
+ export declare function sendResult_plain(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger): Promise<void>;
13
+ export declare function sendResult_forward(session: Session, config: PluginConfig, result: ParsedInfo, logger: Logger, mixed_sending?: boolean): Promise<void>;