yz-yuki-plugin 2.0.8-0 → 2.0.8-10

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.
@@ -156,7 +156,9 @@ class WeiboTask {
156
156
  if (getWhiteWords && Array.isArray(getWhiteWords) && getWhiteWords.length > 0) {
157
157
  // 构建白名单关键字正则表达式,转义特殊字符
158
158
  const whiteWords = new RegExp(getWhiteWords.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
159
- if (!whiteWords.test(`${data?.title}${data?.content}`)) {
159
+ const content = `${data?.title}${data?.content}`;
160
+ if (!whiteWords.test(content)) {
161
+ logger.info(`博主 "${upName}" 微博动态:白名单关键词已开启,但动态消息未匹配,已跳过推送`);
160
162
  return; // 如果动态消息不在白名单中,则直接返回
161
163
  }
162
164
  }
@@ -166,8 +168,11 @@ class WeiboTask {
166
168
  if (getBanWords && Array.isArray(getBanWords) && getBanWords.length > 0) {
167
169
  // 构建屏蔽关键字正则表达式,转义特殊字符
168
170
  const banWords = new RegExp(getBanWords.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
169
- if (banWords.test(`${data?.title}${data?.content}`)) {
170
- return 'return'; // 如果动态消息包含屏蔽关键字,则直接返回
171
+ const content = `${data?.title}${data?.content}`;
172
+ const matched = content.match(banWords);
173
+ if (matched) {
174
+ logger.info(`博主 "${upName}" 微博动态:触发屏蔽关键词 "${matched.join(', ')}" ,已跳过推送`);
175
+ return; // 如果动态消息包含屏蔽关键字,则直接返回
171
176
  }
172
177
  }
173
178
  else if (getBanWords && !Array.isArray(getBanWords)) {
@@ -177,6 +182,7 @@ class WeiboTask {
177
182
  let isSplit = !!weiboConfigData.isSplit === false ? false : true; // 是否启用分片截图,默认为 true
178
183
  let style = isSplit ? '' : `.unfold { max-height: ${weiboConfigData?.noSplitHeight ?? 7500}px; }`; // 不启用分片截图模式的样式
179
184
  let splitHeight = weiboConfigData?.splitHeight ?? 8000; // 分片截图高度,默认 8000, 单位 px,启用分片截图时生效
185
+ let isPauseGif = !!weiboConfigData?.isPauseGif === true ? true : false; // 是否暂停 GIF 动图,默认为 false
180
186
  const extentData = { ...data };
181
187
  const urlQrcodeData = await QRCode.toDataURL(extentData?.url);
182
188
  let renderData = this.buildRenderData(extentData, urlQrcodeData, boxGrid);
@@ -190,7 +196,8 @@ class WeiboTask {
190
196
  quality: 98
191
197
  },
192
198
  saveHtmlfile: false,
193
- pageSplitHeight: splitHeight
199
+ pageSplitHeight: splitHeight,
200
+ isPauseGif: isPauseGif
194
201
  };
195
202
  let imgs = await this.renderDynamicCard(uid, renderData, ScreenshotOptionsData);
196
203
  if (!imgs)
@@ -208,6 +215,7 @@ class WeiboTask {
208
215
  // 构建白名单关键字正则表达式,转义特殊字符
209
216
  const whiteWords = new RegExp(getWhiteWords.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
210
217
  if (!whiteWords.test(dynamicMsg.msg.join(''))) {
218
+ logger.info(`博主 "${upName}" 微博动态:白名单关键词已开启,但动态消息未匹配,已跳过推送`);
211
219
  return; // 如果动态消息不在白名单中,则直接返回
212
220
  }
213
221
  }
@@ -217,8 +225,11 @@ class WeiboTask {
217
225
  if (getBanWords && Array.isArray(getBanWords) && getBanWords.length > 0) {
218
226
  // 构建屏蔽关键字正则表达式,转义特殊字符
219
227
  const banWords = new RegExp(getBanWords.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
220
- if (banWords.test(dynamicMsg.msg.join(''))) {
221
- return 'return'; // 如果动态消息包含屏蔽关键字,则直接返回
228
+ const content = dynamicMsg.msg.join('');
229
+ const matched = content.match(banWords);
230
+ if (matched) {
231
+ logger.info(`博主 "${upName}" 微博动态:触发屏蔽关键词 "${matched.join(', ')}" ,已跳过推送`);
232
+ return; // 如果动态消息包含屏蔽关键字,则直接返回
222
233
  }
223
234
  }
224
235
  else if (getBanWords && !Array.isArray(getBanWords)) {
@@ -245,22 +256,25 @@ class WeiboTask {
245
256
  * @returns 渲染数据
246
257
  */
247
258
  buildRenderData(extentData, urlQrcodeData, boxGrid) {
259
+ const baseData = {
260
+ appName: 'weibo',
261
+ boxGrid: boxGrid,
262
+ type: extentData?.type,
263
+ face: extentData?.face,
264
+ pendant: extentData?.pendant,
265
+ name: extentData?.name,
266
+ pubTs: extentData?.pubTs,
267
+ title: extentData?.title,
268
+ content: extentData?.content,
269
+ urlImgData: urlQrcodeData,
270
+ created: extentData?.created,
271
+ pics: extentData?.pics,
272
+ category: extentData?.category
273
+ };
248
274
  if (extentData.orig && extentData.orig.length !== 0) {
249
275
  return {
250
276
  data: {
251
- appName: 'weibo',
252
- boxGrid: boxGrid,
253
- type: extentData?.type,
254
- face: extentData?.face,
255
- pendant: extentData?.pendant,
256
- name: extentData?.name,
257
- pubTs: extentData?.pubTs,
258
- title: extentData?.title,
259
- content: extentData?.content,
260
- urlImgData: urlQrcodeData,
261
- created: extentData?.created,
262
- pics: extentData?.pics,
263
- category: extentData?.category,
277
+ ...baseData,
264
278
  orig: {
265
279
  data: {
266
280
  type: extentData?.orig?.data?.type,
@@ -278,23 +292,7 @@ class WeiboTask {
278
292
  };
279
293
  }
280
294
  else {
281
- return {
282
- data: {
283
- appName: 'weibo',
284
- boxGrid: boxGrid,
285
- type: extentData?.type,
286
- face: extentData?.face,
287
- pendant: extentData?.pendant,
288
- name: extentData?.name,
289
- pubTs: extentData?.pubTs,
290
- title: extentData?.title,
291
- content: extentData?.content,
292
- urlImgData: urlQrcodeData,
293
- created: extentData?.created,
294
- pics: extentData?.pics,
295
- category: extentData?.category
296
- }
297
- };
295
+ return { data: baseData };
298
296
  }
299
297
  }
300
298
  /**
@@ -0,0 +1,309 @@
1
+ import WeiboApi from './weibo.main.api.js';
2
+ import lodash from 'lodash';
3
+ import crypto from 'crypto';
4
+
5
+ /**
6
+ * 生成更真实的鼠标轨迹数据(贝塞尔曲线模拟)
7
+ * @returns {Array<Array<number>>} 鼠标轨迹数据 [[x偏移, y偏移, 时间戳], ...]
8
+ */
9
+ function generateRealisticMouseTrack() {
10
+ const tracks = [];
11
+ const now = Date.now();
12
+ // 生成最近5分钟内的时间点
13
+ const startTime = now - 5 * 60 * 1000;
14
+ const endTime = now;
15
+ // 起始点和结束点
16
+ const startX = Math.floor(Math.random() * 200);
17
+ const startY = Math.floor(Math.random() * 200);
18
+ const endX = 200 + Math.floor(Math.random() * 800); // 页面常见宽度范围内
19
+ const endY = 100 + Math.floor(Math.random() * 600); // 页面常见高度范围内
20
+ // 控制点(用于贝塞尔曲线)
21
+ const controlX = startX + (endX - startX) * 0.3 + Math.random() * 200 - 100;
22
+ const controlY = startY + (endY - startY) * 0.7 + Math.random() * 200 - 100;
23
+ // 生成轨迹点数量
24
+ const pointCount = 30 + Math.floor(Math.random() * 40);
25
+ // 时间间隔
26
+ let currentTime = startTime;
27
+ const timeStep = (endTime - startTime) / pointCount;
28
+ for (let i = 0; i <= pointCount; i++) {
29
+ const t = i / pointCount;
30
+ // 二次贝塞尔曲线计算当前位置
31
+ const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * controlX + t * t * endX;
32
+ const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * controlY + t * t * endY;
33
+ // 计算与上一个点的偏移量
34
+ if (i === 0) {
35
+ // 第一个点,偏移量为0
36
+ tracks.push([0, 0, Math.floor(currentTime)]);
37
+ }
38
+ else {
39
+ const prevPoint = tracks[tracks.length - 1];
40
+ const deltaX = Math.round(x - (prevPoint[0] + prevPoint[0])); // 简化计算
41
+ const deltaY = Math.round(y - (prevPoint[1] + prevPoint[1]));
42
+ tracks.push([deltaX, deltaY, Math.floor(currentTime)]);
43
+ }
44
+ // 更新时间戳
45
+ currentTime += timeStep + (Math.random() * 200 - 100); // 添加随机波动
46
+ }
47
+ return tracks;
48
+ }
49
+ /**
50
+ * 生成浏览器指纹数据
51
+ * @returns {Object} 包含浏览器指纹信息的对象
52
+ */
53
+ function generateFingerprint() {
54
+ const fingerprintData = { fp: {}, bh: {}, r: {} };
55
+ // 浏览器指纹信息
56
+ fingerprintData.fp = {
57
+ // 版本信息
58
+ 0: '1.2.1',
59
+ // 是否支持某特性(布尔值)
60
+ 1: {
61
+ s: 1,
62
+ v: false
63
+ },
64
+ // 语言信息
65
+ 2: {
66
+ s: 1,
67
+ v: ['lang']
68
+ },
69
+ // User Agent 字符串
70
+ 3: {
71
+ s: 1,
72
+ v: WeiboApi.USER_AGENT.replace(/Mozilla\//g, '')
73
+ },
74
+ // 错误信息
75
+ 4: {
76
+ s: 1,
77
+ v: "TypeError: Cannot read properties of null (reading '0')\n at W (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:14066)\n at Object.NiOqR (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:3108)\n at https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:29253\n at Array.map (<anonymous>)\n at je (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:29193)\n at Object.Qhlex (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:5250)\n at Ie (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:25296)\n at Object.NsuAP (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:4977)\n at Pe (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:24928)\n at Module.Ve [as get] (https://passport.sinaimg.cn/js/fp/1.2.1.umd.js:1:24667)"
78
+ },
79
+ // 数值型数据
80
+ 5: {
81
+ s: 1,
82
+ v: 33
83
+ },
84
+ // 函数字符串表示
85
+ 6: {
86
+ s: 1,
87
+ v: 'function bind() { [native code] }'
88
+ },
89
+ // 语言环境
90
+ 7: {
91
+ s: 1,
92
+ v: [['zh-CN']]
93
+ },
94
+ // 布尔值
95
+ 8: {
96
+ s: 1,
97
+ v: true
98
+ },
99
+ // 布尔值
100
+ 9: {
101
+ s: 1,
102
+ v: false
103
+ },
104
+ // 布尔值
105
+ 10: {
106
+ s: 1,
107
+ v: true
108
+ },
109
+ // 数值
110
+ 11: {
111
+ s: 1,
112
+ v: 5
113
+ },
114
+ // 错误信息
115
+ 12: {
116
+ s: -1,
117
+ e: ''
118
+ },
119
+ // 日期字符串
120
+ 13: {
121
+ s: 1,
122
+ v: '20030107'
123
+ },
124
+ // 数值
125
+ 14: {
126
+ s: 1,
127
+ v: 50
128
+ },
129
+ // 完整的 User Agent
130
+ 15: {
131
+ s: 1,
132
+ v: WeiboApi.USER_AGENT
133
+ },
134
+ // WebGL 信息
135
+ 16: {
136
+ s: 1,
137
+ v: {
138
+ vendor: 'WebKit',
139
+ renderer: 'WebKit WebGL'
140
+ }
141
+ },
142
+ // 外部对象字符串表示
143
+ 17: {
144
+ s: 1,
145
+ v: '[object External]'
146
+ },
147
+ // 屏幕尺寸信息
148
+ 18: {
149
+ s: 1,
150
+ v: {
151
+ ow: 1920,
152
+ oh: 1152,
153
+ iw: 257,
154
+ ih: 1031
155
+ }
156
+ },
157
+ // 浏览器名称
158
+ 19: {
159
+ s: 1,
160
+ v: 'chrome'
161
+ },
162
+ // 浏览器内核
163
+ 20: {
164
+ s: 1,
165
+ v: 'chromium'
166
+ },
167
+ // 布尔值
168
+ 21: {
169
+ s: 1,
170
+ v: false
171
+ },
172
+ // 布尔值
173
+ 22: {
174
+ s: 1,
175
+ v: true
176
+ },
177
+ // 触摸支持信息
178
+ 23: {
179
+ s: 1,
180
+ v: {
181
+ ots: false,
182
+ mtp: 0,
183
+ mmtp: -1
184
+ }
185
+ }
186
+ };
187
+ // 行为数据(鼠标轨迹和键盘操作)
188
+ fingerprintData.bh = {
189
+ // 鼠标移动轨迹 [x偏移, y偏移, 时间戳毫秒]
190
+ mt: generateRealisticMouseTrack(),
191
+ // 键盘操作统计
192
+ kt: {
193
+ down: 0,
194
+ up: 0
195
+ }
196
+ };
197
+ // 追踪设置
198
+ fingerprintData.r = {
199
+ isTraceKeyboard: true,
200
+ isTraceMouse: true
201
+ };
202
+ return fingerprintData;
203
+ }
204
+ /**
205
+ * 生成 AES 加密密钥和初始化向量
206
+ * @returns {Promise<Object>} 包含 key 和 iv 的对象
207
+ */
208
+ function generateAESKey() {
209
+ // 生成 AES 密钥 (16位)
210
+ const key = crypto.randomBytes(16);
211
+ // 生成初始化向量
212
+ const iv = crypto.randomBytes(16);
213
+ return {
214
+ key: Buffer.from(key).toString('base64'),
215
+ iv: Buffer.from(iv).toString('base64')
216
+ };
217
+ }
218
+ /**
219
+ * RSA 加密函数 (模拟浏览器 Web Crypto API)
220
+ * @param {string} data - 待加密数据
221
+ * @param {Uint8Array} publicKeyDer - DER 格式的公钥
222
+ * @returns {Buffer} 加密后的数据
223
+ */
224
+ function rsaEncrypt(data, publicKeyDer) {
225
+ // 将 DER 格式的公钥转换为 PEM 格式
226
+ const base64Key = Buffer.from(publicKeyDer).toString('base64');
227
+ const keyLines = base64Key.match(/.{1,64}/g) ?? [];
228
+ const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${keyLines.join('\n')}\n-----END PUBLIC KEY-----`;
229
+ // 使用 RSA-OAEP-SHA256 算法加密
230
+ return crypto.publicEncrypt({
231
+ key: publicKeyPem,
232
+ padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
233
+ oaepHash: 'sha256'
234
+ }, Buffer.from(data, 'binary'));
235
+ }
236
+ /**
237
+ * AES CBC 加密函数 (模拟浏览器 Web Crypto API)
238
+ * @param {string} data - 待加密数据
239
+ * @param {string} keyBase64 - Base64 编码的密钥
240
+ * @param {string} ivBase64 - Base64 编码的初始化向量
241
+ * @returns {string} Base64 编码的加密结果
242
+ */
243
+ function aesEncrypt(data, keyBase64, ivBase64) {
244
+ const key = Buffer.from(keyBase64, 'base64');
245
+ const iv = Buffer.from(ivBase64, 'base64');
246
+ const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
247
+ cipher.setAutoPadding(true);
248
+ let encrypted = cipher.update(data, 'utf8', 'binary');
249
+ encrypted += cipher.final('binary');
250
+ return Buffer.from(encrypted, 'binary').toString('base64');
251
+ }
252
+ /**
253
+ * 数据加密主函数
254
+ * @param {string} jsonData - JSON 格式的指纹数据
255
+ * @returns {Promise<string>} 加密后的数据
256
+ */
257
+ async function encryptData(jsonData) {
258
+ // 原始的 RSA 公钥(DER 格式),与源代码完全一致
259
+ const publicKeyDer = new Uint8Array([
260
+ 48, 129, 159, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 129, 141, 0, 48, 129, 137, 2, 129, 129, 0, 180, 249, 101, 74, 227, 247, 222, 230,
261
+ 24, 220, 10, 149, 183, 131, 164, 185, 20, 166, 164, 114, 158, 71, 46, 151, 77, 71, 226, 23, 78, 67, 177, 246, 197, 249, 213, 39, 243, 55, 38, 112, 17, 64,
262
+ 135, 155, 109, 50, 185, 61, 21, 105, 106, 245, 148, 212, 127, 7, 18, 227, 255, 40, 199, 241, 65, 211, 167, 185, 232, 5, 186, 189, 245, 59, 161, 214, 48,
263
+ 160, 251, 21, 92, 187, 172, 83, 152, 11, 85, 72, 37, 137, 87, 104, 63, 39, 86, 6, 150, 84, 6, 178, 229, 220, 144, 133, 131, 212, 47, 139, 232, 185, 192, 97,
264
+ 89, 137, 170, 141, 39, 19, 85, 4, 153, 238, 75, 93, 243, 96, 206, 72, 135, 91, 2, 3, 1, 0, 1
265
+ ]);
266
+ // 生成 AES 密钥和 IV
267
+ const { key, iv } = generateAESKey();
268
+ // RSA 加密 AES 密钥(将密钥重复两次后加密)
269
+ const keyBinary = Buffer.from(key, 'base64').toString('binary');
270
+ const encryptedKey = rsaEncrypt(keyBinary + keyBinary, publicKeyDer);
271
+ // AES 加密数据
272
+ const encryptedData = aesEncrypt(jsonData, key, iv);
273
+ // 组合加密数据(与原始代码保持一致的格式)
274
+ const result = '01' + Buffer.from('01' + encryptedKey.toString('binary') + '02' + Buffer.from(encryptedData, 'base64').toString('binary')).toString('base64');
275
+ return result;
276
+ }
277
+ /**
278
+ * 生成请求载荷
279
+ * @returns {Promise<string>} 加密后的载荷数据
280
+ */
281
+ async function genBdPayload() {
282
+ const fingerprint = generateFingerprint();
283
+ const jsonString = JSON.stringify(fingerprint);
284
+ const encryptedData = await encryptData(jsonString);
285
+ return encryptedData;
286
+ }
287
+ /**
288
+ * 访问bd接口获取rid
289
+ * @param {string} X_CSRF_TOKEN - X_CSRF_TOKEN
290
+ * @returns {Promise<JSON>} 服务器响应结果
291
+ */
292
+ async function getRidFromBd(X_CSRF_TOKEN) {
293
+ const params = new URLSearchParams();
294
+ const payload = await genBdPayload();
295
+ payload && params.append('data', payload);
296
+ params.append('from', 'weibo');
297
+ const ridData = (await fetch('https://passport.weibo.com/sso/bd', {
298
+ method: 'POST',
299
+ headers: lodash.merge(WeiboApi.WEIBO_GET_BD_TOKEN_HEADERS, {
300
+ Origin: 'https://passport.weibo.com',
301
+ Cookie: `X-CSRF-TOKEN=${X_CSRF_TOKEN}`
302
+ }),
303
+ body: params,
304
+ redirect: 'follow'
305
+ }).then(res => res.json()));
306
+ return ridData;
307
+ }
308
+
309
+ export { genBdPayload, getRidFromBd };
@@ -53,7 +53,9 @@ class YukiPuppeteerRender {
53
53
  pageHeight = Math.round(boundingBox.height / num); //动态调整分片高度,防止过短影响观感。
54
54
  await page.setViewport({ width: boundingBox.width + 50, height: pageHeight + 100 });
55
55
  // 禁止 GIF 动图播放
56
- await page.addStyleTag({ content: `img[src$=".gif"] {animation-play-state: paused !important;}` });
56
+ if (Options?.isPauseGif === true) {
57
+ await page.addStyleTag({ content: `img[src$=".gif"] {animation-play-state: paused !important;}` });
58
+ }
57
59
  // 是否保存 html 文件
58
60
  if (Options?.saveHtmlfile === true) {
59
61
  const htmlContent = await page.content();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yz-yuki-plugin",
3
- "version": "2.0.8-0",
3
+ "version": "2.0.8-10",
4
4
  "description": "优纪插件,yunzaijs 关于 微博推送、B站推送 等功能的拓展插件",
5
5
  "author": "snowtafir",
6
6
  "type": "module",
@@ -28,10 +28,11 @@
28
28
  "axios": "^1.7.9",
29
29
  "chalk": "^5.3.0",
30
30
  "chokidar": "4.0.1",
31
+ "cookie": "^1.0.2",
31
32
  "debug": "^4.3.6",
32
33
  "jsdom": "^25.0.1",
33
34
  "json5": "^2.2.3",
34
- "jsxp": "^1.2.1",
35
+ "jsxp": "^1.2.3",
35
36
  "lodash": "^4.17.21",
36
37
  "md5": "^2.3.0",
37
38
  "moment": "^2.30.1",
@@ -63,9 +64,9 @@
63
64
  "icqq": "^0.6.10",
64
65
  "jsdom": "^24.1.1",
65
66
  "json5": "^2.2.3",
66
- "jsxp": "^1.2.1",
67
+ "jsxp": "^1.2.3",
67
68
  "lodash": "^4.17.21",
68
- "lvyjs": "^0.2.19",
69
+ "lvyjs": "^0.2.21",
69
70
  "md5": "^2.3.0",
70
71
  "node-fetch": "^3.3.2",
71
72
  "postcss": "^8.4.47",
@@ -28,7 +28,7 @@ body::-webkit-scrollbar {
28
28
  @import url('https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css');
29
29
 
30
30
  body {
31
- font-family: 'OPSans', 'HarmonyOS_Regular', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
31
+ font-family: 'OPSans', 'HarmonyOS_Regular', 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
32
32
  background-color: #f9f9f9;
33
33
  margin: 0;
34
34
  padding: 0;